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
26 changes: 26 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,32 @@ jest.mock('expo-asset', () => ({
},
}));

// Mock @sentry/react-native to prevent native module errors in tests
jest.mock('@sentry/react-native', () => {
const breadcrumbs: any[] = [];

return {
__esModule: true,
init: jest.fn().mockResolvedValue(undefined),
wrap: jest.fn((component: any) => component),
captureException: jest.fn(),
captureMessage: jest.fn(),
addBreadcrumb: jest.fn().mockImplementation((breadcrumb: any) => {
breadcrumbs.push(breadcrumb);
}),
reactNavigationIntegration: jest.fn(() => ({
registerNavigationContainer: jest.fn(),
})),
ErrorBoundary: ({ children }: any) => children,
setUser: jest.fn(),
setTag: jest.fn(),
setExtra: jest.fn(),
isInitialized: jest.fn().mockReturnValue(true),
// Re-export breadcrumbs for test assertions
_breadcrumbs: breadcrumbs,
};
});

// Mock expo-av to avoid import issues
jest.mock('expo-av', () => ({
Audio: {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@
"@react-native-community/netinfo": "11.4.1",
"@rnmapbox/maps": "10.2.10",
"@semantic-release/git": "^10.0.1",
"@sentry/react-native": "~7.2.0",
"@sentry/react-native": "~8.10.0",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "~5.52.1",
"app-icon-badge": "^0.1.2",
"axios": "^1.12.0",
"babel-plugin-module-resolver": "^5.0.2",
"buffer": "^6.0.3",
"countly-sdk-react-native-bridge": "^25.4.0",
"countly-sdk-react-native-bridge": "^25.4.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"expo": "^54.0.33",
Expand Down
19 changes: 9 additions & 10 deletions src/app/(app)/__tests__/map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-camera')).toBeTruthy();
});

it('shows side menu in landscape mode', async () => {
it('renders in landscape mode', async () => {
// Mock landscape dimensions
const mockUseWindowDimensions = (jest.requireMock('react-native') as any).useWindowDimensions;
mockUseWindowDimensions.mockReturnValue({
Expand All @@ -368,23 +368,21 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});

// In landscape mode, side menu should be permanently visible
expect(screen.getByTestId('side-menu')).toBeTruthy();
// In landscape mode, the map container should still render correctly
// (the side menu is now handled by the parent _layout.tsx, not the map page itself)
expect(screen.getByTestId('home-map-container')).toBeTruthy();
});

it('shows drawer in portrait mode when opened', async () => {
it('does not render its own drawer in portrait mode', async () => {
render(<HomeMap />);

// Wait for async map data to load
await waitFor(() => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});

// Initially drawer should not be visible
// The drawer is now handled by the parent _layout.tsx, not the map page itself
expect(screen.queryByTestId('drawer')).toBeNull();

// Since there's no header menu button, we can't test opening the drawer
// This test would need to be modified based on how the drawer is actually opened
});

it('shows recenter button when user has moved map and location is available', async () => {
Expand Down Expand Up @@ -548,8 +546,9 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});

// In landscape mode, side menu should be permanently visible
expect(screen.getByTestId('side-menu')).toBeTruthy();
// In landscape mode, the map container should still render correctly
// (side menu is handled by the parent _layout.tsx)
expect(screen.getByTestId('home-map-container')).toBeTruthy();
});

describe('Analytics Tracking', () => {
Expand Down
73 changes: 15 additions & 58 deletions src/app/(app)/map.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Menu } from 'lucide-react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { useWindowDimensions, View } from 'react-native';

import MapPanel from '@/components/maps/map-panel';
import PoiListPanel from '@/components/maps/poi-list-panel';
import { SideMenu } from '@/components/sidebar/side-menu';
import { Button, ButtonText } from '@/components/ui/button';
import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index';
import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
import { SharedTabs, type TabItem } from '@/components/ui/shared-tabs';
import { type PoiResultData } from '@/models/v4/mapping/poiResultData';
Expand All @@ -20,7 +16,6 @@ export default function HomeMap() {
const { tab, poiId } = useLocalSearchParams<{ tab?: string | string[]; poiId?: string | string[] }>();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
const fetchPoisData = usePoiStore((state) => state.fetchPoisData);

useEffect(() => {
Expand Down Expand Up @@ -80,59 +75,21 @@ export default function HomeMap() {
<View className="size-full flex-1 bg-neutral-100 dark:bg-neutral-950" testID="home-map-container">
<FocusAwareStatusBar />

{/* Content Area with Side Menu */}
<View className="flex-1 flex-row p-4">
{/* Permanent Side Menu in Landscape */}
{isLandscape && (
<View className="mr-4 w-[280px] overflow-hidden rounded-[28px] border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
<SideMenu />
</View>
)}

{/* Map Content */}
<View className="flex-1">
{/* Portrait menu button */}
{!isLandscape && (
<TouchableOpacity
accessibilityLabel={t('map.openSideMenu')}
accessibilityRole="button"
className="mb-3 self-start rounded-xl border border-neutral-200 bg-white p-2.5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900"
onPress={() => setIsSideMenuOpen(true)}
>
<Menu size={22} className="text-neutral-700 dark:text-neutral-300" />
</TouchableOpacity>
)}
<SharedTabs
key={selectedTab || 'map'}
tabs={tabs}
initialIndex={initialTabIndex}
variant="segmented"
size={isLandscape ? 'lg' : 'md'}
scrollable={false}
tabsContainerClassName="rounded-2xl border border-neutral-200 bg-white p-1.5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900"
tabClassName="rounded-xl"
contentClassName="pt-4"
/>
</View>
{/* Map Content */}
<View className="flex-1 p-4">
<SharedTabs
key={selectedTab || 'map'}
tabs={tabs}
initialIndex={initialTabIndex}
variant="segmented"
size={isLandscape ? 'lg' : 'md'}
scrollable={false}
tabsContainerClassName="rounded-2xl border border-neutral-200 bg-white p-1.5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900"
tabClassName="rounded-xl"
contentClassName="pt-4"
/>
</View>
</View>

{/* Drawer for Portrait Mode */}
{!isLandscape && (
<Drawer isOpen={isSideMenuOpen} onClose={() => setIsSideMenuOpen(false)} size="lg">
<DrawerBackdrop onPress={() => setIsSideMenuOpen(false)} />
<DrawerContent className="bg-white dark:bg-gray-900">
<DrawerBody className="p-0">
<SideMenu onNavigate={() => setIsSideMenuOpen(false)} />
</DrawerBody>
<DrawerFooter className="border-t border-gray-200 p-3 dark:border-gray-800">
<Button onPress={() => setIsSideMenuOpen(false)} className="w-full bg-primary-600">
<ButtonText>{t('common.close')}</ButtonText>
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</>
);
}
26 changes: 2 additions & 24 deletions src/app/(app)/messages.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { useFocusEffect } from '@react-navigation/native';
import { router, Stack } from 'expo-router';
import { ChevronDown, Mail, MailOpen, Menu, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native';
import { ChevronDown, Mail, MailOpen, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, useWindowDimensions } from 'react-native';
import { Alert } from 'react-native';

import { Loading } from '@/components/common/loading';
import ZeroState from '@/components/common/zero-state';
import { ComposeMessageSheet } from '@/components/messages/compose-message-sheet';
import { MessageCard } from '@/components/messages/message-card';
import { MessageDetailsSheet } from '@/components/messages/message-details-sheet';
import { SideMenu } from '@/components/sidebar/side-menu';
import { FocusAwareStatusBar, View } from '@/components/ui';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetItem, ActionsheetItemText } from '@/components/ui/actionsheet';
import { Badge } from '@/components/ui/badge';
import { Button, ButtonText } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index';
import { Fab, FabIcon } from '@/components/ui/fab';
import { FlatList } from '@/components/ui/flat-list';
import { HStack } from '@/components/ui/hstack';
Expand All @@ -32,11 +30,8 @@ import { useSecurityStore } from '@/stores/security/store';

export default function MessagesScreen() {
const { t } = useTranslation();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);

const { canUserCreateMessages } = useSecurityStore();
const { trackEvent } = useAnalytics();
Expand Down Expand Up @@ -211,13 +206,6 @@ export default function MessagesScreen() {
<Stack.Screen
options={{
title: t('messages.title'),
headerLeft: () =>
!isLandscape ? (
<Pressable className="p-2" onPress={() => setIsSideMenuOpen(true)} testID="messages-menu-button">
<Menu size={24} className="text-gray-700 dark:text-gray-300" />
</Pressable>
) : null,
headerRight: () => null,
}}
/>
<FocusAwareStatusBar />
Expand Down Expand Up @@ -331,16 +319,6 @@ export default function MessagesScreen() {

{isLoading && filteredMessages.length === 0 && <Loading />}

{/* Side Menu Drawer */}
<Drawer isOpen={isSideMenuOpen} onClose={() => setIsSideMenuOpen(false)} size={isLandscape ? 'md' : 'lg'}>
<DrawerBackdrop onPress={() => setIsSideMenuOpen(false)} />
<DrawerContent>
<DrawerBody className="p-0">
<SideMenu onNavigate={() => setIsSideMenuOpen(false)} />
</DrawerBody>
</DrawerContent>
</Drawer>

{/* Filter Action Sheet */}
<Actionsheet isOpen={isFilterMenuOpen} onClose={() => setIsFilterMenuOpen(false)}>
<ActionsheetBackdrop />
Expand Down
22 changes: 17 additions & 5 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,33 @@ const navigationIntegration = Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: false,
});

// Validate DSN is configured — warn early if missing
if (!Env.SENTRY_DSN) {
console.warn('[Sentry] DSN is empty — errors and profiles will NOT be sent. Set RESPOND_SENTRY_DSN in your environment.');
}

Sentry.init({
dsn: Env.SENTRY_DSN,
debug: __DEV__, // Only debug in development, not production
tracesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact
profilesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact
// tracesSampleRate: percentage of transactions sent (1.0 = 100%, 0.2 = 20%)
tracesSampleRate: __DEV__ ? 1.0 : 0.2,
// profilesSampleRate: relative to tracesSampleRate!
// Effective profile rate = tracesSampleRate * profilesSampleRate.
// In production: 0.2 * 1.0 = 20% of all transactions get profiles.
// Set to 1.0 to profile ALL sampled transactions, or lower to further sub-sample.
profilesSampleRate: __DEV__ ? 1.0 : 1.0,
sendDefaultPii: false,
enableAppHangTracking: true, // v8: Tracks app freeze/hang events
enableWatchdogTerminationTracking: true, // v8: Tracks iOS watchdog terminations
integrations: [
// Pass integration
navigationIntegration,
],
enableNativeFramesTracking: true, //!isRunningInExpoGo(), // Tracks slow and frozen frames in the application
enableNativeFramesTracking: true, // Tracks slow and frozen frames
// Add additional options to prevent timing issues
beforeSendTransaction(event: any) {
beforeSend(event) {
// Filter out problematic navigation transactions that might cause timestamp errors
if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
if (event.type === 'transaction' && event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
return null;
Comment on lines +82 to 85
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In @sentry/react-native8.x, are transaction events filtered throughbeforeSendorbeforeSendTransaction?

💡 Result:

In @sentry/react-native 8.x, transaction events are filtered through beforeSendTransaction, not beforeSend. The beforeSend callback filters error events from the JavaScript layer, while beforeSendTransaction specifically filters transaction events.[1][2] Official documentation explicitly shows beforeSendTransaction as the configuration option for transaction filtering in React Native SDK, including code examples for use in Sentry.init().[1] This behavior has been consistent since earlier versions (e.g., 4.9.0) and carries over to 8.x with no breaking changes noted in release notes or migration guides affecting it.[3][4][5]

Citations:


🏁 Script executed:

cat -n src/app/_layout.tsx | sed -n '75,95p'

Repository: Resgrid/Responder

Length of output: 1947


Use beforeSendTransaction for transaction filtering.

Line 82 currently filters event.type === 'transaction' inside beforeSend, but according to official Sentry documentation for @sentry/react-native, transaction events are filtered through beforeSendTransaction, not beforeSend. The beforeSend callback only filters error events, so this transaction guard will never apply. Move this filter to beforeSendTransaction:

Suggested fix
 Sentry.init({
   dsn: Env.SENTRY_DSN,
   debug: __DEV__,
   tracesSampleRate: __DEV__ ? 1.0 : 0.2,
   profilesSampleRate: __DEV__ ? 1.0 : 1.0,
   sendDefaultPii: false,
   enableAppHangTracking: true,
   enableWatchdogTerminationTracking: true,
   integrations: [
     navigationIntegration,
   ],
   enableNativeFramesTracking: true,
-  beforeSend(event) {
-    // Filter out problematic navigation transactions that might cause timestamp errors
-    if (event.type === 'transaction' && event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
-      return null;
-    }
-    return event;
-  },
+  beforeSendTransaction(event) {
+    if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
+      return null;
+    }
+    return event;
+  },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeSend(event) {
// Filter out problematic navigation transactions that might cause timestamp errors
if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
if (event.type === 'transaction' && event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
return null;
Sentry.init({
dsn: Env.SENTRY_DSN,
debug: __DEV__,
tracesSampleRate: __DEV__ ? 1.0 : 0.2,
profilesSampleRate: __DEV__ ? 1.0 : 1.0,
sendDefaultPii: false,
enableAppHangTracking: true,
enableWatchdogTerminationTracking: true,
integrations: [
navigationIntegration,
],
enableNativeFramesTracking: true,
beforeSendTransaction(event) {
if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
return null;
}
return event;
},
});
🤖 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/_layout.tsx` around lines 82 - 85, The transaction filter currently
inside beforeSend (checking event.type === 'transaction' and the
contexts.trace.op/data.route guard) will never run for transactions; move that
logic into the beforeSendTransaction callback instead and remove it from
beforeSend: locate the existing beforeSend implementation and extract the
navigation transaction guard (the event.type === 'transaction' &&
event.contexts?.trace?.op === 'navigation' &&
!event.contexts?.trace?.data?.route check) into a new or existing
beforeSendTransaction handler so transactions are properly filtered by Sentry;
keep any other error-event filtering in beforeSend unchanged.

}
return event;
Expand Down
2 changes: 1 addition & 1 deletion src/app/call/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ export default function CallDetail() {

{/* Tabs */}
<Box className={`mt-4 flex-1 pb-8 ${colorScheme === 'dark' ? 'bg-neutral-900' : 'bg-neutral-100'}`}>
<SharedTabs tabs={renderTabs()} variant="underlined" size={isLandscape ? 'md' : 'sm'} />
<SharedTabs tabs={renderTabs()} variant="underlined" size={isLandscape ? 'md' : 'sm'} scrollable={!isLandscape} />
</Box>
</ScrollView>
<CallNotesModal isOpen={isNotesModalOpen} onClose={() => setIsNotesModalOpen(false)} callId={callId || ''} />
Expand Down
7 changes: 2 additions & 5 deletions src/components/maps/map-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Loading } from '@/components/common/loading';
import { useAnalytics } from '@/hooks/use-analytics';
import { useMapSignalRUpdates } from '@/hooks/use-map-signalr-updates';
import { logger } from '@/lib/logging';
import { isPoiMarker } from '@/lib/poi';
import { onSortOptions } from '@/lib/utils';
import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
import { type PoiResultData } from '@/models/v4/mapping/poiResultData';
Expand All @@ -20,8 +21,6 @@ import { useToastStore } from '@/stores/toast/store';
import MapPins from './map-pins';
import PinDetailModal from './pin-detail-modal';

const POI_MARKER_TYPE = 4;

interface MapPanelProps {
focusedPoi: PoiResultData | null;
}
Expand Down Expand Up @@ -266,9 +265,7 @@ export const MapPanel: React.FC<MapPanelProps> = ({ focusedPoi }) => {
pinType: pin.Type,
});

const isPoiPin = pin.Type === POI_MARKER_TYPE || pin.PoiTypeId != null;

if (isPoiPin) {
if (isPoiMarker(pin)) {
router.push(`/poi/${pin.Id}`);
return;
}
Expand Down
Loading
Loading