mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
Compare commits
3
Commits
0f9c9c2e15
...
15c7894bec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c7894bec | ||
|
|
57c7ecebe6 | ||
|
|
17372a9bf0 |
No files matched your search
@@ -179,7 +179,10 @@ describe.skipIf(!url || !local)('dares + PostgreSQL', () => {
|
|||||||
const dir = new URL('../drizzle/', import.meta.url);
|
const dir = new URL('../drizzle/', import.meta.url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await migrate(drizzle(migrationPool), { migrationsFolder: fileURLToPath(dir) });
|
await migrate(drizzle(migrationPool), {
|
||||||
|
migrationsFolder: fileURLToPath(dir),
|
||||||
|
migrationsSchema: migrationSchema,
|
||||||
|
});
|
||||||
const discovered = await migrationPool.query<{ count: string }>(
|
const discovered = await migrationPool.query<{ count: string }>(
|
||||||
"select count(*)::text as count from dares where id between '1f5e0000-0000-4000-8000-000000000001'::uuid and '1f5e0000-0000-4000-8000-000000000150'::uuid",
|
"select count(*)::text as count from dares where id between '1f5e0000-0000-4000-8000-000000000001'::uuid and '1f5e0000-0000-4000-8000-000000000150'::uuid",
|
||||||
);
|
);
|
||||||
|
|||||||
+11
-32
@@ -21,7 +21,7 @@ import {
|
|||||||
handleNotificationResponse,
|
handleNotificationResponse,
|
||||||
} from "../src/lib/notifications";
|
} from "../src/lib/notifications";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Platform, View, AppState, AppStateStatus, Modal, Text, StyleSheet, Pressable, TouchableOpacity } from "react-native";
|
import { Platform, View, AppState, AppStateStatus, Modal, Text, StyleSheet, Pressable, TouchableOpacity } from "react-native";
|
||||||
|
|
||||||
// Suppress useLayoutEffect warning on server-side rendering
|
// Suppress useLayoutEffect warning on server-side rendering
|
||||||
@@ -51,7 +51,7 @@ import * as Linking from "expo-linking";
|
|||||||
import { useAuthStore } from "../src/store";
|
import { useAuthStore } from "../src/store";
|
||||||
import { authClient } from "../src/lib/authClient";
|
import { authClient } from "../src/lib/authClient";
|
||||||
import { colors, spacing, radius, typography } from "../src/theme";
|
import { colors, spacing, radius, typography } from "../src/theme";
|
||||||
import { hasSeenGuestAccountWarning, markGuestAccountWarningSeen } from "../src/lib/guestAccountWarningSeen";
|
import { useGuestAccountWarning } from "../src/hooks/useGuestAccountWarning";
|
||||||
import { parseInviteLinkCode } from "../src/lib/inviteLink";
|
import { parseInviteLinkCode } from "../src/lib/inviteLink";
|
||||||
import { stashPendingInviteCode } from "../src/lib/pendingInviteCode";
|
import { stashPendingInviteCode } from "../src/lib/pendingInviteCode";
|
||||||
import { needsOnboarding } from "../src/constants/onboarding";
|
import { needsOnboarding } from "../src/constants/onboarding";
|
||||||
@@ -59,18 +59,17 @@ import { needsOnboarding } from "../src/constants/onboarding";
|
|||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const { fetchUser, setUser, isAuthenticated, isAnonymous } = useAuthStore();
|
const { fetchUser, setUser, user, isAuthenticated, isAnonymous } = useAuthStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
const analyticsInitialized = useRef(false);
|
const analyticsInitialized = useRef(false);
|
||||||
|
|
||||||
const [showGuestWarning, setShowGuestWarning] = useState(false);
|
const { visible: showGuestWarning, dismiss: dismissGuestWarning } = useGuestAccountWarning({
|
||||||
const [checkingGuestWarning, setCheckingGuestWarning] = useState(false);
|
isAuthenticated,
|
||||||
|
isAnonymous,
|
||||||
const dismissGuestWarning = async () => {
|
userId: user?.id,
|
||||||
await markGuestAccountWarningSeen();
|
pathname,
|
||||||
setShowGuestWarning(false);
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Initialize analytics after native modules are ready
|
// Initialize analytics after native modules are ready
|
||||||
@@ -90,22 +89,6 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
// Warn guest users (first time only) that accounts are not recoverable
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAuthenticated || !isAnonymous) return;
|
|
||||||
if (checkingGuestWarning) return;
|
|
||||||
if (pathname?.startsWith("/(auth)")) return;
|
|
||||||
|
|
||||||
setCheckingGuestWarning(true);
|
|
||||||
hasSeenGuestAccountWarning()
|
|
||||||
.then((seen) => {
|
|
||||||
if (!seen) {
|
|
||||||
setShowGuestWarning(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => setCheckingGuestWarning(false));
|
|
||||||
}, [isAuthenticated, isAnonymous, pathname, checkingGuestWarning]);
|
|
||||||
|
|
||||||
// Track warm starts when app comes back from background
|
// Track warm starts when app comes back from background
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = AppState.addEventListener("change", (nextAppState: AppStateStatus) => {
|
const subscription = AppState.addEventListener("change", (nextAppState: AppStateStatus) => {
|
||||||
@@ -315,8 +298,7 @@ export default function RootLayout() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.guestWarningPrimary}
|
style={styles.guestWarningPrimary}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
await markGuestAccountWarningSeen();
|
await dismissGuestWarning();
|
||||||
setShowGuestWarning(false);
|
|
||||||
router.push("/(app)/settings/save-account" as any);
|
router.push("/(app)/settings/save-account" as any);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
@@ -325,10 +307,7 @@ export default function RootLayout() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.guestWarningSecondary}
|
style={styles.guestWarningSecondary}
|
||||||
onPress={async () => {
|
onPress={dismissGuestWarning}
|
||||||
await markGuestAccountWarningSeen();
|
|
||||||
setShowGuestWarning(false);
|
|
||||||
}}
|
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={styles.guestWarningSecondaryText}>Not now</Text>
|
<Text style={styles.guestWarningSecondaryText}>Not now</Text>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { act, renderHook, waitFor } from "@testing-library/react-native";
|
||||||
|
import { hasSeenGuestAccountWarning, markGuestAccountWarningSeen } from "@/lib/guestAccountWarningSeen";
|
||||||
|
import { useGuestAccountWarning } from "@/hooks/useGuestAccountWarning";
|
||||||
|
|
||||||
|
jest.mock("@/lib/guestAccountWarningSeen", () => ({
|
||||||
|
hasSeenGuestAccountWarning: jest.fn(),
|
||||||
|
markGuestAccountWarningSeen: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
isAuthenticated: true,
|
||||||
|
isAnonymous: true,
|
||||||
|
userId: "guest-1",
|
||||||
|
pathname: "/(app)/pairing",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("useGuestAccountWarning", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.mocked(hasSeenGuestAccountWarning).mockReset();
|
||||||
|
jest.mocked(markGuestAccountWarningSeen).mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reopen after a dismissal while the durable write is pending", async () => {
|
||||||
|
let finishMarking: (() => void) | undefined;
|
||||||
|
jest.mocked(hasSeenGuestAccountWarning).mockResolvedValue(false);
|
||||||
|
jest.mocked(markGuestAccountWarningSeen).mockImplementation(() => new Promise<void>((resolve) => {
|
||||||
|
finishMarking = resolve;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook((props: typeof options) => useGuestAccountWarning(props), { initialProps: options });
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.visible).toBe(true));
|
||||||
|
|
||||||
|
let dismissal: Promise<void> | undefined;
|
||||||
|
act(() => {
|
||||||
|
dismissal = result.current.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.visible).toBe(false);
|
||||||
|
|
||||||
|
rerender({ ...options, pathname: "/(app)/settings" });
|
||||||
|
expect(result.current.visible).toBe(false);
|
||||||
|
expect(hasSeenGuestAccountWarning).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
finishMarking?.();
|
||||||
|
await act(async () => {
|
||||||
|
await dismissal;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks once per anonymous account and can warn a later guest account", async () => {
|
||||||
|
jest.mocked(hasSeenGuestAccountWarning).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook((props: typeof options) => useGuestAccountWarning(props), { initialProps: options });
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.visible).toBe(true));
|
||||||
|
rerender({ ...options, pathname: "/(app)/settings" });
|
||||||
|
expect(hasSeenGuestAccountWarning).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender({ ...options, userId: "guest-2" });
|
||||||
|
await waitFor(() => expect(hasSeenGuestAccountWarning).toHaveBeenCalledWith("guest-2"));
|
||||||
|
await waitFor(() => expect(result.current.visible).toBe(true));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { hasSeenGuestAccountWarning, markGuestAccountWarningSeen } from "@/lib/guestAccountWarningSeen";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
|
describe("guestAccountWarningSeen", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return AsyncStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists a dismissal for that anonymous account", async () => {
|
||||||
|
await markGuestAccountWarningSeen("guest-1");
|
||||||
|
|
||||||
|
await expect(hasSeenGuestAccountWarning("guest-1")).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not suppress the recovery warning for a later guest account", async () => {
|
||||||
|
await markGuestAccountWarningSeen("guest-1");
|
||||||
|
|
||||||
|
await expect(hasSeenGuestAccountWarning("guest-2")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { hasSeenGuestAccountWarning, markGuestAccountWarningSeen } from "../lib/guestAccountWarningSeen";
|
||||||
|
|
||||||
|
type GuestAccountWarningOptions = {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isAnonymous: boolean;
|
||||||
|
userId: string | null | undefined;
|
||||||
|
pathname: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the account recovery warning at most once for each anonymous account
|
||||||
|
* during a mounted app session. A dismissal is applied in memory before the
|
||||||
|
* durable write completes, so a slow storage operation cannot reopen it.
|
||||||
|
*/
|
||||||
|
export function useGuestAccountWarning({
|
||||||
|
isAuthenticated,
|
||||||
|
isAnonymous,
|
||||||
|
userId,
|
||||||
|
pathname,
|
||||||
|
}: GuestAccountWarningOptions) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const checkedUserIds = useRef(new Set<string>());
|
||||||
|
const dismissedUserIds = useRef(new Set<string>());
|
||||||
|
|
||||||
|
const isEligible = Boolean(
|
||||||
|
isAuthenticated
|
||||||
|
&& isAnonymous
|
||||||
|
&& userId
|
||||||
|
&& !pathname?.startsWith("/(auth)"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEligible || !userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedUserIds.current.has(userId) || dismissedUserIds.current.has(userId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkedUserIds.current.add(userId);
|
||||||
|
let isCurrent = true;
|
||||||
|
|
||||||
|
void hasSeenGuestAccountWarning(userId).then((seen) => {
|
||||||
|
if (isCurrent && !seen && !dismissedUserIds.current.has(userId)) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCurrent = false;
|
||||||
|
};
|
||||||
|
}, [isEligible, userId]);
|
||||||
|
|
||||||
|
const dismiss = useCallback(async () => {
|
||||||
|
if (!userId) {
|
||||||
|
setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissedUserIds.current.add(userId);
|
||||||
|
setVisible(false);
|
||||||
|
await markGuestAccountWarningSeen(userId);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return { visible: isEligible && visible, dismiss };
|
||||||
|
}
|
||||||
@@ -1,47 +1,31 @@
|
|||||||
import * as SecureStore from "expo-secure-store";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { Platform } from "react-native";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "guest_account_recovery_warning_seen";
|
const STORAGE_KEY_PREFIX = "guest_account_recovery_warning_seen";
|
||||||
|
|
||||||
async function getStorage(): Promise<boolean> {
|
const buildKey = (userId: string): string => `${STORAGE_KEY_PREFIX}_${userId}`;
|
||||||
|
|
||||||
|
async function getStorage(key: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
let data: string | null = null;
|
return (await AsyncStorage.getItem(key)) === "true";
|
||||||
|
|
||||||
if (Platform.OS === "web") {
|
|
||||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
||||||
data = window.localStorage.getItem(STORAGE_KEY);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data = await SecureStore.getItemAsync(STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data === "true";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading guest warning state:", error);
|
console.error("Error reading guest warning state:", error);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setStorage(seen: boolean): Promise<void> {
|
async function setStorage(key: string, seen: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const data = seen ? "true" : "false";
|
await AsyncStorage.setItem(key, seen ? "true" : "false");
|
||||||
|
|
||||||
if (Platform.OS === "web") {
|
|
||||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await SecureStore.setItemAsync(STORAGE_KEY, data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving guest warning state:", error);
|
console.error("Error saving guest warning state:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasSeenGuestAccountWarning(): Promise<boolean> {
|
/** The recovery warning is shown at most once for each anonymous account. */
|
||||||
return getStorage();
|
export async function hasSeenGuestAccountWarning(userId: string): Promise<boolean> {
|
||||||
|
return getStorage(buildKey(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markGuestAccountWarningSeen(): Promise<void> {
|
export async function markGuestAccountWarningSeen(userId: string): Promise<void> {
|
||||||
await setStorage(true);
|
await setStorage(buildKey(userId), true);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { redactInvitePaths } from './providers'
|
import type { CapturedNetworkRequest } from 'posthog-js'
|
||||||
|
import { redactInviteNetworkRequest, redactInvitePaths } from './providers'
|
||||||
|
|
||||||
const pageViewPath = new URL('./PostHogPageView.tsx', import.meta.url)
|
const pageViewPath = new URL('./PostHogPageView.tsx', import.meta.url)
|
||||||
const providerPath = new URL('./providers.tsx', import.meta.url)
|
const providerPath = new URL('./providers.tsx', import.meta.url)
|
||||||
@@ -20,6 +21,7 @@ describe('PostHog invite privacy boundary', () => {
|
|||||||
expect(source).toContain('capture_pageleave: false')
|
expect(source).toContain('capture_pageleave: false')
|
||||||
expect(source).toContain('get_current_url: redactInviteUrl')
|
expect(source).toContain('get_current_url: redactInviteUrl')
|
||||||
expect(source).toContain('before_send: redactInvitePathsBeforeSend')
|
expect(source).toContain('before_send: redactInvitePathsBeforeSend')
|
||||||
|
expect(source).toContain('maskCapturedNetworkRequestFn: redactInviteNetworkRequest')
|
||||||
expect(source).toContain("url.pathname.startsWith('/join/')")
|
expect(source).toContain("url.pathname.startsWith('/join/')")
|
||||||
expect(source).toContain('/join/[invite]')
|
expect(source).toContain('/join/[invite]')
|
||||||
})
|
})
|
||||||
@@ -43,4 +45,32 @@ describe('PostHog invite privacy boundary', () => {
|
|||||||
expect(serialized).toContain('/join/[invite]')
|
expect(serialized).toContain('/join/[invite]')
|
||||||
expect(serialized).toContain('join_page_viewed')
|
expect(serialized).toContain('join_page_viewed')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('redacts invite codes from replay network request URLs, headers, and bodies without mutating the capture', () => {
|
||||||
|
const inviteCode = 'A4K9BT2Q'
|
||||||
|
const request = {
|
||||||
|
name: `https://sauci.app/join/${inviteCode}`,
|
||||||
|
entryType: 'resource',
|
||||||
|
startTime: 1,
|
||||||
|
duration: 2,
|
||||||
|
requestHeaders: {
|
||||||
|
referer: `https://sauci.app/join/${inviteCode}`,
|
||||||
|
},
|
||||||
|
requestBody: JSON.stringify({ invite_path: `/join/${inviteCode}` }),
|
||||||
|
responseHeaders: {
|
||||||
|
location: `https://sauci.app/join/${inviteCode}`,
|
||||||
|
},
|
||||||
|
responseBody: JSON.stringify({ referrer: `https://sauci.app/join/${inviteCode}` }),
|
||||||
|
} as CapturedNetworkRequest
|
||||||
|
|
||||||
|
const redacted = redactInviteNetworkRequest(request)
|
||||||
|
const serialized = JSON.stringify(redacted)
|
||||||
|
|
||||||
|
expect(redacted).not.toBe(request)
|
||||||
|
expect(redacted.requestHeaders).not.toBe(request.requestHeaders)
|
||||||
|
expect(redacted.responseHeaders).not.toBe(request.responseHeaders)
|
||||||
|
expect(serialized).not.toContain(inviteCode)
|
||||||
|
expect(serialized).toContain('/join/[invite]')
|
||||||
|
expect(JSON.stringify(request)).toContain(inviteCode)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||||
import type { CaptureResult } from 'posthog-js'
|
import type { CapturedNetworkRequest, CaptureResult } from 'posthog-js'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
@@ -38,6 +38,17 @@ export function redactInvitePathsBeforeSend(event: CaptureResult | null): Captur
|
|||||||
return event ? redactInvitePaths(event) : null
|
return event ? redactInvitePaths(event) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function redactInviteNetworkRequest(request: CapturedNetworkRequest): CapturedNetworkRequest {
|
||||||
|
return {
|
||||||
|
...request,
|
||||||
|
name: redactInvitePaths(request.name),
|
||||||
|
requestHeaders: request.requestHeaders ? redactInvitePaths({ ...request.requestHeaders }) : request.requestHeaders,
|
||||||
|
requestBody: redactInvitePaths(request.requestBody),
|
||||||
|
responseHeaders: request.responseHeaders ? redactInvitePaths({ ...request.responseHeaders }) : request.responseHeaders,
|
||||||
|
responseBody: redactInvitePaths(request.responseBody),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
@@ -51,6 +62,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||||||
capture_pageleave: false,
|
capture_pageleave: false,
|
||||||
get_current_url: redactInviteUrl,
|
get_current_url: redactInviteUrl,
|
||||||
before_send: redactInvitePathsBeforeSend,
|
before_send: redactInvitePathsBeforeSend,
|
||||||
|
session_recording: {
|
||||||
|
maskCapturedNetworkRequestFn: redactInviteNetworkRequest,
|
||||||
|
},
|
||||||
// This must be part of initialization, not only the navigation effect
|
// This must be part of initialization, not only the navigation effect
|
||||||
// below, so an initially loaded invite page is protected before its DOM
|
// below, so an initially loaded invite page is protected before its DOM
|
||||||
// can be considered for capture or recording.
|
// can be considered for capture or recording.
|
||||||
|
|||||||
Reference in new issue
Block a user