Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/api/chat/chatbot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChatbotChannelResponse, type ChatbotSendResponse, type ChatbotSessionResponse } from '@/models/v4/chat';
import { type ChatbotChannelResponse, type ChatbotSendResponse, type ChatbotSessionResponse, type IncidentAssistantAnswerResponse, type IncidentAssistantSuggestionsResponse } from '@/models/v4/chat';

import { api } from '../common/client';

Expand Down Expand Up @@ -27,3 +27,19 @@ export const newChatbotSession = async () => {
const response = await api.post<ChatbotSessionResponse>(`${CHATBOT}/NewChatSession`, {});
return response.data;
};

/**
* Asks the incident assistant a command-board question and gets the answer back in the same
* round-trip. `callId` scopes the question to the board the caller has open, so "PAR" resolves
* against that incident rather than guessing among the department's active commands.
*/
export const askIncidentAssistant = async (callId: number, question: string, signal?: AbortSignal) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Missing JSDoc return documentation: askIncidentAssistant omits a formal @returns {Promise<Type>} tag and rejection conditions. Rule [22] requires async functions to document the resolve value, rejection conditions, and await usage with @returns {Promise<...>}.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/api/chat/chatbot.ts:

Line 36:

Missing JSDoc return documentation: `askIncidentAssistant` omits a formal `@returns {Promise<Type>}` tag and rejection conditions. Rule [22] requires async functions to document the resolve value, rejection conditions, and await usage with `@returns {Promise<...>}`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

const response = await api.post<IncidentAssistantAnswerResponse>(`${CHATBOT}/AskIncident`, { Question: question, CallId: callId }, { signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Missing error handling: this external HTTP call to api.post has no try/catch. Rule [27] requires network/external calls to be wrapped in try/catch with context (callId/question) added and errors mapped to application-level errors.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/api/chat/chatbot.ts:

Line 37:

Missing error handling: this external HTTP call to `api.post` has no try/catch. Rule [27] requires network/external calls to be wrapped in try/catch with context (`callId`/`question`) added and errors mapped to application-level errors.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

return response.data?.Data ?? null;
};

/** Server-side suggested questions for an incident, from the ICS playbook it infers for the call. */
export const getIncidentAssistantSuggestions = async (callId: number, signal?: AbortSignal) => {
const response = await api.get<IncidentAssistantSuggestionsResponse>(`${CHATBOT}/IncidentSuggestions`, { params: { callId }, signal });
return response.data?.Data ?? null;
Comment on lines +36 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the approved API endpoint factories.

These methods call the API client directly. Define them through createApiEndpoint or createCachedApiEndpoint so they follow the required API module boundary. Preserve the typed response and AbortSignal behavior.

As per coding guidelines, “Implement API modules with createApiEndpoint or createCachedApiEndpoint, use typed generics on HTTP methods, and invalidate relevant caches after mutations.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/chat/chatbot.ts` around lines 36 - 44, Refactor askIncidentAssistant
and getIncidentAssistantSuggestions to use the approved createApiEndpoint or
createCachedApiEndpoint factories instead of calling api.post/api.get directly.
Preserve their typed response generics, request payloads, query parameters,
return values, and optional AbortSignal propagation.

Source: Coding guidelines

};
140 changes: 140 additions & 0 deletions src/app/(app)/__tests__/init-session-generation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Signing out while app initialization is still awaiting must retire that run: a stale
* invocation may not mark the app initialized, connect the chat hub, or restart location
* tracking that the sign-out cleanup just stopped.
*
* The layout itself pulls in Mapbox, Novu, push notifications and the whole store graph,
* so the guard protocol is exercised through the same generation-token shape the layout
* uses rather than by rendering it.
*/
import { act, renderHook } from '@testing-library/react-native';
import React from 'react';

interface Deferred {
promise: Promise<void>;
resolve: () => void;
}

function deferred(): Deferred {
let resolve: () => void = () => undefined;
const promise = new Promise<void>((res) => {
resolve = res;
});
return { promise, resolve };
}

/** Mirrors the layout's initializeApp guard: generation captured at start, checked after each await. */
function useInitGuard(gate: Deferred, effects: { connectHub: jest.Mock; startLocation: jest.Mock; markInitialized: jest.Mock }) {
const initGeneration = React.useRef(0);
const isInitializing = React.useRef(false);

const initialize = React.useCallback(async () => {
if (isInitializing.current) return;
isInitializing.current = true;
const generation = (initGeneration.current += 1);
const isCurrentRun = () => initGeneration.current === generation;

try {
await gate.promise;
if (!isCurrentRun()) return;

effects.connectHub();
if (!isCurrentRun()) return;

effects.markInitialized();
if (!isCurrentRun()) return;

effects.startLocation();
} finally {
if (isCurrentRun()) {
isInitializing.current = false;
}
}
}, [gate, effects]);

const signOut = React.useCallback(() => {
initGeneration.current += 1;
isInitializing.current = false;
}, []);

return { initialize, signOut, isInitializing };
}

describe('app initialization session generation', () => {
const effects = { connectHub: jest.fn(), startLocation: jest.fn(), markInitialized: jest.fn() };

beforeEach(() => {
jest.clearAllMocks();
});

it('abandons an in-flight run when the session ends mid-initialization', async () => {
const gate = deferred();
const { result } = renderHook(() => useInitGuard(gate, effects));

let pending: Promise<void> = Promise.resolve();
act(() => {
pending = result.current.initialize();
});

// Sign-out lands while initialization is still awaiting its first step.
act(() => {
result.current.signOut();
});

await act(async () => {
gate.resolve();
await pending;
});

expect(effects.connectHub).not.toHaveBeenCalled();
expect(effects.markInitialized).not.toHaveBeenCalled();
expect(effects.startLocation).not.toHaveBeenCalled();
});

it('completes normally when the session survives', async () => {
const gate = deferred();
const { result } = renderHook(() => useInitGuard(gate, effects));

let pending: Promise<void> = Promise.resolve();
act(() => {
pending = result.current.initialize();
});

await act(async () => {
gate.resolve();
await pending;
});

expect(effects.connectHub).toHaveBeenCalledTimes(1);
expect(effects.markInitialized).toHaveBeenCalledTimes(1);
expect(effects.startLocation).toHaveBeenCalledTimes(1);
});

it('frees the in-progress guard so the next sign-in can initialize', async () => {
const first = deferred();
const { result } = renderHook(() => useInitGuard(first, effects));

let pending: Promise<void> = Promise.resolve();
act(() => {
pending = result.current.initialize();
});
act(() => {
result.current.signOut();
});

// The new session starts before the retired run has settled.
let second: Promise<void> = Promise.resolve();
act(() => {
second = result.current.initialize();
});

await act(async () => {
first.resolve();
await Promise.all([pending, second]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Promise.all([pending, second]) aborts remaining tasks on first rejection instead of settling all items independently. Replace with Promise.allSettled and handle per-item results for batch operations with partial failures.

Kody rule violation: Use Promise.allSettled for batch operations with partial failures

Prompt for LLM

File src/app/(app)/__tests__/init-session-generation.test.tsx:

Line 133:

`Promise.all([pending, second])` aborts remaining tasks on first rejection instead of settling all items independently. Replace with `Promise.allSettled` and handle per-item results for batch operations with partial failures.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

});

// Exactly one run reached the effects: the current one.
expect(effects.markInitialized).toHaveBeenCalledTimes(1);
expect(effects.startLocation).toHaveBeenCalledTimes(1);
});
Comment on lines +70 to +139

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Unmount each hook after the test.

Each renderHook call leaves cleanup to the test library. Capture and call unmount() during cleanup.

As per coding guidelines, src/**/*.test.{ts,tsx} must “call unmount() to clean up.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/__tests__/init-session-generation.test.tsx around lines 70 -
139, Capture the unmount function returned by each renderHook call in the three
useInitGuard tests, and call unmount() during each test’s cleanup after
assertions or awaited work completes. Preserve the existing initialization and
sign-out behavior while ensuring every rendered hook is explicitly unmounted.

Source: Coding guidelines

});
34 changes: 31 additions & 3 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export default function TabLayout() {
// Refs to track initialization state
const hasInitialized = useRef(false);
const isInitializing = useRef(false);
// Bumped on every initialization start and whenever the session ends. An in-flight run
// compares its captured value after each await, so a run belonging to a session that is
// over can no longer connect hubs or mark the app initialized.
const initGeneration = useRef(0);
const hasHiddenSplash = useRef(false);
const parentRef = useRef(null);

Expand Down Expand Up @@ -155,6 +159,8 @@ export default function TabLayout() {
}

isInitializing.current = true;
const generation = (initGeneration.current += 1);
const isCurrentRun = () => initGeneration.current === generation;
logger.info({
message: 'Starting app initialization',
context: {
Expand All @@ -171,9 +177,13 @@ export default function TabLayout() {
await securityStore.getState().getRights();
await featureFlagsStore.getState().fetchFlags();

if (!isCurrentRun()) return;

await useSignalRStore.getState().connectUpdateHub();
await useSignalRStore.getState().connectGeolocationHub();

if (!isCurrentRun()) return;

// Connect the realtime chat hub (best-effort; chat may be disabled per department)
if (featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem)) {
try {
Expand All @@ -192,6 +202,8 @@ export default function TabLayout() {
.syncFromServer()
.catch(() => {});

if (!isCurrentRun()) return;

hasInitialized.current = true;

// Initialize Bluetooth and Audio services (native-only)
Expand All @@ -208,12 +220,20 @@ export default function TabLayout() {
message: 'Failed to initialize app',
context: { error },
});
// A run whose session already ended must not burn the retry budget or clobber
// state a newer run has since established.
if (!isCurrentRun()) return;

// Reset initialization state on error so it can be retried
hasInitialized.current = false;
setInitRetryCount((c) => c + 1);
} finally {
isInitializing.current = false;
setIsInitComplete(true);
// Only the current run owns the guard; a superseded run clearing it would let two
// initializations overlap.
if (isCurrentRun()) {
isInitializing.current = false;
setIsInitComplete(true);
}
}
}, [status]);

Expand Down Expand Up @@ -261,7 +281,15 @@ export default function TabLayout() {
// Handle app initialization - simplified logic
const MAX_INIT_RETRIES = 3;
useEffect(() => {
if (status !== 'signedIn' && initRetryCount > 0) {
if (status === 'signedIn') return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Raw string literal 'signedIn' represents a finite-set auth status inline, lacking the type safety and discoverability already established by constants like FeatureFlagKeys.ChatSystem. Extract it into an AuthStatus enum or as const object and reference AuthStatus.SignedIn in the comparison.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/app/(app)/_layout.tsx:

Line 284:

Raw string literal `'signedIn'` represents a finite-set auth status inline, lacking the type safety and discoverability already established by constants like `FeatureFlagKeys.ChatSystem`. Extract it into an `AuthStatus` enum or `as const` object and reference `AuthStatus.SignedIn` in the comparison.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Shared string literal 'signedIn' is inlined directly in the comparison, creating duplication risk across consumers. Extract it into a centralized constant (e.g., export const SIGNED_IN = 'signedIn') and import it here so every reference points to a single source of truth.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/app/(app)/_layout.tsx:

Line 284:

Shared string literal `'signedIn'` is inlined directly in the comparison, creating duplication risk across consumers. Extract it into a centralized constant (e.g., `export const SIGNED_IN = 'signedIn'`) and import it here so every reference points to a single source of truth.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


// Leaving the signed-in state retires any initialization still in flight, and frees
// the guard it no longer owns so the next sign-in is not skipped as "already
// initializing".
initGeneration.current += 1;
isInitializing.current = false;

if (initRetryCount > 0) {
setInitRetryCount(0);
}
}, [status, initRetryCount]);
Expand Down
39 changes: 2 additions & 37 deletions src/app/(app)/chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
import { MessageActionsSheet } from '@/components/chat/message-actions-sheet';
import { MessageBubble } from '@/components/chat/message-bubble';
import { TypingDots } from '@/components/chat/typing-indicator';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '@/components/ui/actionsheet';
import { Box } from '@/components/ui/box';
import { Button, ButtonText } from '@/components/ui/button';
import { Center } from '@/components/ui/center';
import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
import { HStack } from '@/components/ui/hstack';
Expand All @@ -19,7 +17,6 @@
import { Pressable } from '@/components/ui/pressable';
import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { Textarea, TextareaInput } from '@/components/ui/textarea';
import { VStack } from '@/components/ui/vstack';
import { type ChatMessageResultData } from '@/models/v4/chat';
import useAuthStore from '@/stores/auth/store';
Expand All @@ -37,8 +34,6 @@
const isModerator = !!securityStore((s) => s.rights)?.IsAdmin;
const [text, setText] = useState('');
const [actionsMessage, setActionsMessage] = useState<ChatMessageResultData | null>(null);
const [editMessage, setEditMessage] = useState<ChatMessageResultData | null>(null);
const [editText, setEditText] = useState('');
const chatStatus = useChatSystemStatus();
const isChatEnabled = chatStatus === 'enabled';

Expand Down Expand Up @@ -72,7 +67,7 @@

const renderItem = useCallback(
({ item }: { item: ChatMessageResultData }) => (
<MessageBubble message={item} isOwn={!!item.SenderUserId && item.SenderUserId === currentUserId} showSender={false} currentUserId={currentUserId} onLongPress={setActionsMessage} onToggleReaction={() => undefined} />

Check warning on line 70 in src/app/(app)/chatbot.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·message={item}·isOwn={!!item.SenderUserId·&&·item.SenderUserId·===·currentUserId}·showSender={false}·currentUserId={currentUserId}·onLongPress={setActionsMessage}·onToggleReaction={()·=>·undefined}` with `⏎········message={item}⏎········isOwn={!!item.SenderUserId·&&·item.SenderUserId·===·currentUserId}⏎········showSender={false}⏎········currentUserId={currentUserId}⏎········onLongPress={setActionsMessage}⏎········onToggleReaction={()·=>·undefined}⏎·····`
),
[currentUserId]
);
Expand Down Expand Up @@ -146,7 +141,7 @@
</HStack>
</KeyboardAvoidingView>

{/* Restricted actions for assistant messages: copy, edit own, pin (moderator), flag. */}
{/* Restricted actions for assistant messages: copy, pin (moderator), flag. */}
<MessageActionsSheet
message={actionsMessage}
isOpen={actionsMessage !== null}
Expand All @@ -160,42 +155,12 @@
const ok = await copyToClipboard(m.Body ?? '');
useToastStore.getState().showToast(ok ? 'success' : 'info', ok ? t('chat.copied') : t('chat.copy_unavailable'));
}}
onEdit={(m) => {
setEditMessage(m);
setEditText(m.Body ?? '');
}}
onEdit={() => undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Inline arrow function in JSX prop creates a new function on every render, impacting performance. Move function definitions outside the render method or extract to a stable reference.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 158:

Inline arrow function in JSX prop creates a new function on every render, impacting performance. Move function definitions outside the render method or extract to a stable reference.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

onDelete={() => undefined}
onFlag={(m, reason) => useChatStore.getState().flagMessage(m.ChatMessageId, reason)}
onTogglePin={(m, pinned) => chatbotChannelId && useChatStore.getState().togglePin(m.ChatMessageId, chatbotChannelId, pinned)}
onModeratorDelete={() => undefined}
/>

{/* Edit own message */}
<Actionsheet isOpen={editMessage !== null} onClose={() => setEditMessage(null)}>
<ActionsheetBackdrop />
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<VStack className="w-full p-2" space="md">
<Text className="text-base font-semibold text-typography-900">{t('chat.edit_message')}</Text>
<Textarea>
<TextareaInput value={editText} onChangeText={setEditText} multiline />
</Textarea>
<Button
className="bg-primary-600"
onPress={() => {
if (editMessage && chatbotChannelId && editText.trim()) {
void useChatStore.getState().editMessage(editMessage.ChatMessageId, chatbotChannelId, editText.trim());
}
setEditMessage(null);
}}
>
<ButtonText>{t('chat.save')}</ButtonText>
</Button>
</VStack>
</ActionsheetContent>
</Actionsheet>
</Box>
);
}
12 changes: 11 additions & 1 deletion src/app/(app)/command.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { router } from 'expo-router';
import { ClipboardList, CloudOff, ExternalLink, Image as ImageIcon, Info, MapPin, Paperclip, Pencil, RefreshCw, StickyNote, Trash2, UserCog, Video as VideoIcon, XCircle } from 'lucide-react-native';
import { ClipboardList, CloudOff, ExternalLink, Image as ImageIcon, Info, MapPin, Paperclip, Pencil, RefreshCw, Sparkles, StickyNote, Trash2, UserCog, Video as VideoIcon, XCircle } from 'lucide-react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, useWindowDimensions } from 'react-native';
Expand All @@ -13,6 +13,7 @@ import { AddAssignmentSheet } from '@/components/command/add-assignment-sheet';
import { AddLaneSheet } from '@/components/command/add-lane-sheet';
import { AddResourceSheet } from '@/components/command/add-resource-sheet';
import { type AssignableResourceOption, AssignResourceSheet } from '@/components/command/assign-resource-sheet';
import { IncidentAssistantSheet } from '@/components/command/assistant-sheet';
import { CommandDetailsSheet } from '@/components/command/command-details-sheet';
import { CommandSection } from '@/components/command/command-section';
import { IncidentFilesSection } from '@/components/command/incident-files-section';
Expand Down Expand Up @@ -141,6 +142,7 @@ export default function CommandBoard() {
const [resourceFilter, setResourceFilter] = useState<'all' | 'unassigned' | 'assigned'>('all');
const [isCommandDetailsOpen, setIsCommandDetailsOpen] = useState(false);
const [isEndConfirmOpen, setIsEndConfirmOpen] = useState(false);
const [isAssistantOpen, setIsAssistantOpen] = useState(false);
/** Which call-resource viewer (from the underlying call) is open on top of the board. */
const [callResourceModal, setCallResourceModal] = useState<'notes' | 'images' | 'files' | 'video' | null>(null);

Expand Down Expand Up @@ -204,6 +206,8 @@ export default function CommandBoard() {
}
}, [activeBoardCallId]);

const handleOpenAssistant = useCallback(() => setIsAssistantOpen(true), []);
const handleCloseAssistant = useCallback(() => setIsAssistantOpen(false), []);
const handleOpenCommandDetails = useCallback(() => setIsCommandDetailsOpen(true), []);
const handleOpenTransfer = useCallback(() => setIsTransferSheetOpen(true), []);
const handleOpenEndConfirm = useCallback(() => setIsEndConfirmOpen(true), []);
Expand Down Expand Up @@ -597,6 +601,10 @@ export default function CommandBoard() {
<Button onPress={handleRefresh} variant="outline" size="xs" isDisabled={isRefreshing} testID="command-refresh">
<ButtonIcon as={RefreshCw} />
</Button>
{/* Assistant: answers board questions on-device first, so it stays useful with no signal */}
<Button onPress={handleOpenAssistant} variant="outline" size="xs" accessibilityLabel={t('incident_assistant.title')} testID="command-assistant">
<ButtonIcon as={Sparkles} />
</Button>
{/* Icon-only by design; a confirmation dialog guards against accidental taps. */}
<Button onPress={handleOpenEndConfirm} action="negative" variant="solid" size="xs" accessibilityLabel={t('command.end_command')} testID="command-end-command">
<ButtonIcon as={XCircle} className="text-white" />
Expand Down Expand Up @@ -945,6 +953,8 @@ export default function CommandBoard() {
onSave={handleAssignResourceSave}
/>

<IncidentAssistantSheet isOpen={isAssistantOpen} onClose={handleCloseAssistant} callId={boardState.callId} />

{/* Call resource viewers — the same modals the call detail screen uses, opened in place */}
<CallNotesModal isOpen={callResourceModal === 'notes'} onClose={() => setCallResourceModal(null)} callId={boardState.callId} />
<CallImagesModal isOpen={callResourceModal === 'images'} onClose={() => setCallResourceModal(null)} callId={boardState.callId} />
Expand Down
8 changes: 7 additions & 1 deletion src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ const navigationIntegration = Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: false,
});

// Sentry's own logger is off by default: watchdog-termination tracking rewrites the
// native scope on every RNSentry turbo-module call, so `debug` floods the Metro
// console with hundreds of "Writing tags to disk" lines a second. Flip to `__DEV__`
// temporarily when diagnosing Sentry itself.
const SENTRY_DEBUG = false;

Sentry.init({
dsn: Env.SENTRY_DSN,
debug: __DEV__, // Only debug in development, not production
debug: SENTRY_DEBUG,
tracesSampleRate: __DEV__ ? 0.1 : 0.2, // 10% in dev (low to avoid setTimeout wrapping overhead), 20% in production
profilesSampleRate: __DEV__ ? 0.1 : 0.2, // 10% in dev, 20% in production
sendDefaultPii: false,
Expand Down
Loading
Loading