Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Aug 19, 2025

Summary by CodeRabbit

  • New Features
    • Added in-app push notification modal.
    • App icon badge enabled for non-production builds.
    • Initialized offline queue on app startup.
  • Improvements
    • Extensive analytics added across Home, Map, Calls, Messages, Calendar, Contacts, Shifts, Units, Onboarding, Login, and Settings.
    • Permission-based UI: compose actions and call menus now respect user rights.
    • PII protection: personnel contact info and personnel map pins hidden without permission.
    • Enhanced audio/bluetooth device selection UX and telemetry; dark-mode icon styling updates.
    • Notification Inbox stability with config safeguards.
  • Chores
    • Migrated analytics to Aptabase; updated environment variables and dependencies.
    • Android CI/CD release automation improvements.
  • Documentation
    • Added analytics and audio/secure storage configuration guides.

@coderabbitai
Copy link

coderabbitai bot commented Aug 19, 2025

Walkthrough

Broad update introducing Aptabase analytics across many screens/components, PII-based UI gating, Bluetooth/audio UX and telemetry, new status/offline queue stores, env/config changes, and CI/CD Android release automation. Significant Android native files were removed. Extensive tests and docs added; some mocks and libraries were replaced or deleted.

Changes

Cohort / File(s) Summary
CI/CD Workflow
.github/workflows/react-native-cicd.yml
Android versionCode derived from run_number; jq-based package.json update; Android-only GitHub Release with RELEASE_NOTES extraction; duplicate release blocks present.
Android Native Removal
android/... (build.gradle, settings.gradle, gradle wrapper, app/src/main/AndroidManifest.xml, res/, values/, proguard, .gitignore)
Deleted app and project Gradle configs, manifest, resources, and wrapper files.
Env & Configuration
env.js, .env.development, app.config.ts, jest.config.js
Replaced PostHog keys with Aptabase keys; optional STORAGE_ENCRYPTION_KEY; Expo config updated (versioning from package.json, plugins incl. app-icon-badge, BLE manager); Jest transforms @aptabase.
Dependency Updates
package.json
Added Aptabase, callkeep config plugin, NetInfo, app-icon-badge, crypto-js, secure-store; swapped react-native-ble-plx for react-native-ble-manager; removed posthog-react-native.
Analytics Hook & Provider
src/hooks/use-analytics.ts, src/components/common/aptabase-provider.tsx, src/components/common/posthog-provider.tsx (deleted)
Added useAnalytics delegating to aptabaseService; added AptabaseProviderWrapper; removed PostHog provider.
App Initialization
src/app/_layout.tsx, src/app/(app)/_layout.tsx
Switched analytics to Aptabase; added appInitializationService, PushNotificationModal; initialized offline queue in (app) layout; Sentry tuning.
API Config
src/api/config/index.ts
Added getSystemConfig endpoint; stopped encoding key in getConfig.
Status Stores & UI
src/components/status/store.ts, src/components/status/status-bottom-sheet.tsx (deleted), src/components/status/__tests__/personnel-status-bottom-sheet.test.tsx
New Zustand stores for status flow and saving (with offline queue); removed legacy status bottom sheet; tests adjusted for new store surface.
LiveKit Call Store
src/features/livekit-call/store/useLiveKitCallStore.ts, __tests__/useLiveKitCallStore.test.ts
Integrated iOS CallKeep lifecycle, logging, expanded state; connection commented out; comprehensive tests added.
Bluetooth & Audio
src/components/bluetooth/bluetooth-audio-modal.tsx, src/components/settings/bluetooth-device-selection-bottom-sheet.tsx, src/components/settings/audio-device-selection.tsx, tests under `src/components/(bluetooth
settings)/**`
Maps PII Gating
src/components/maps/map-pins.tsx, src/components/maps/pin-detail-modal.tsx, tests under src/components/maps/__tests__/*
Filter personnel pins when canUserViewPII is false; hide personnel coordinates; added tests for PII behavior.
Personnel PII Gating
src/components/personnel/personnel-card.tsx, src/components/personnel/personnel-details-sheet.tsx, tests under src/components/personnel/__tests__/*
Hide contact info when PII not allowed; tests added (some duplicated blocks noted).
Notifications
src/components/notifications/NotificationInbox.tsx, tests under src/components/notifications/__tests__/*
Import moved to lib/auth; early config guards; zIndex/pointerEvents tweaks; tests updated (simple suite removed, main suite expanded).
Screens Analytics
src/app/(app)/**/* (calendar, contacts, home, map, messages, calls, shifts, settings, units, login, onboarding; plus call/[id], call/new, edit flows)
Added useAnalytics and focus-based view events; comprehensive interaction events (search, filters, FABs, modals, routing); numerous tests added validating payloads.
Audio Stream Analytics
src/components/audio-stream/audio-stream-bottom-sheet.tsx, tests
Added view/start/stop/refresh/close/error events; tests verify payloads.
Common Components
src/components/common/loading.tsx, src/components/common/zero-state.tsx, src/components/settings/item.tsx, src/components/notifications/NotificationButton.tsx
Loading removed testID prop; ZeroState adds viewClassName/centerClassName; minor icon styling changes; removed colorScheme-based arrow styling.
Messages Security & Analytics
src/app/(app)/messages.tsx, tests under src/app/(app)/__tests__/*messages*
Gate compose actions by canUserCreateMessages; added analytics across interactions; tests cover security gating and tracking.
Calls Security & Analytics
src/app/(app)/home/calls.tsx, src/app/call/[id].tsx, components and tests under src/components/calls/** and src/app/call/**
Gate FAB/header menu by canUserCreateCalls; track view/actions; tests updated/added.
Map & Calendar Analytics Docs/Tests
docs/map-analytics-implementation.md, src/app/(app)/__tests__/map.test.tsx, src/components/calendar/*
Documentation and tests for analytics events and payloads.
Docs Added
docs/*analytics*.md, docs/secure-storage-implementation.md, docs/audio-permissions-configuration-update.md
New documentation for analytics implementations and secure storage.
Docs Removed
docs/bluetooth-*.md, docs/bluetooth-audio-integration.md
Removed older Bluetooth feature docs.
Mocks
__mocks__/react-native-ble-plx.ts (deleted), __mocks__/react-native-gesture-handler.ts
Removed BLE PLX mock; updated RNGH mock path.
Misc
src/components/sidebar/side-menu.tsx, simple-test.js
Removed unused toggleMicrophone; deleted simple test stub.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Screen
  participant Focus as useFocusEffect
  participant Hook as useAnalytics
  participant Service as aptabaseService

  Screen->>Focus: onFocus callback
  Focus-->>Screen: invoked on focus
  Screen->>Hook: const { trackEvent }
  Screen->>Service: trackEvent('event_name', {timestamp, ...})
  Service-->>Screen: fire-and-forget (no blocking)
Loading
sequenceDiagram
  autonumber
  participant UI as DeviceSelectionSheet
  participant Store as BluetoothAudioStore
  participant Svc as bluetooth-audio.service
  participant Pref as usePreferredBluetoothDevice
  participant Analytics as aptabaseService

  UI->>Analytics: bluetooth_device_selection_sheet_viewed
  UI->>Svc: startScanning(10000)
  UI->>Pref: setPreferredDevice(null)
  alt connected device exists
    UI->>Svc: disconnectDevice(prevId)
  end
  UI->>Pref: setPreferredDevice(newDevice)
  UI->>Svc: connectToDevice(newDevice.id)
  UI->>Analytics: bluetooth_device_selection_completed {...}
  UI-->>UI: close sheet
Loading
sequenceDiagram
  autonumber
  participant App as App Root
  participant Init as appInitializationService
  participant Offline as offlineQueueService
  participant Logger as logger

  App->>Init: initialize()
  Init-->>App: ok/fail (logged)
  App->>Offline: initialize()
  Offline-->>App: ok/fail (logged)
  App-->>Logger: "App initialization completed"
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Poem

A rabbit taps the keys with glee,
Analytics sprout like clover free.
Pins hide PII in shaded burrow,
Bluetooth hops from weak to thorough.
Android hills, now trimmed and bare,
New trails for queues and status care.
Thump—release! A tagged affair. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 34> [!CAUTION]

Some comments are outside the diff and can’t be posted inline due to platform limits.

🔭 Outside diff range comments (10)
src/components/status/__tests__/personnel-status-bottom-sheet.test.tsx (2)

654-654: Fix false negative: query the exact label including colon

Earlier assertions use the label with a trailing colon (custom_responding_to:). This check will pass even if the label is still rendered, because it omits the colon.

-      expect(screen.queryByText('personnel.status.custom_responding_to')).toBeNull();
+      expect(screen.queryByText('personnel.status.custom_responding_to:')).toBeNull();

672-672: Fix false negative: query the exact “note” label including colon

Same issue as above for the “note” label.

-      expect(screen.queryByText('personnel.status.note')).toBeNull();
+      expect(screen.queryByText('personnel.status.note:')).toBeNull();
src/components/notifications/__tests__/NotificationInbox.test.tsx (1)

342-354: Test name doesn't match its implementation.

The test is named "shows success toast on successful delete" but doesn't actually verify that a toast is shown. It only directly calls the delete API without going through the component's delete flow.

Either rename the test to match what it actually does, or implement the proper toast verification:

-  it('shows success toast on successful delete', async () => {
+  it('calls delete API successfully', async () => {
     mockDeleteMessage.mockResolvedValue(undefined);

-    const { getByText } = render(
-      <NotificationInbox isOpen={true} onClose={mockOnClose} />
-    );
-
     await act(async () => {
       await deleteMessage('1');
     });

     expect(mockDeleteMessage).toHaveBeenCalledWith('1');
   });

Or implement proper toast verification through the component's delete flow.

app.config.ts (1)

176-180: Fix typos and product name inconsistencies in user-facing strings

  • “Resgird” → “Resgrid”
  • “Resgrid Unit” → “Resgrid Responder” (align with the rest of the app’s wording)

Apply this diff:

-        locationWhenInUsePermission: 'Allow Resgird Responder to show current location on map.',
+        locationWhenInUsePermission: 'Allow Resgrid Responder to show current location on map.',
@@
-      NSBluetoothAlwaysUsageDescription: 'Allow Resgrid Unit to connect to bluetooth devices for PTT.',
+      NSBluetoothAlwaysUsageDescription: 'Allow Resgrid Responder to connect to Bluetooth devices for PTT.',
@@
-      {
-        microphonePermission: 'Allow Resgrid Unit to access the microphone for audio input used in PTT and calls.',
-      },
+      {
+        microphonePermission: 'Allow Resgrid Responder to access the microphone for audio input used in PTT and calls.',
+      },

Also applies to: 51-52, 256-259

src/app/call/[id].tsx (1)

492-495: Typo in property name: rename ImgagesCountImagesCount across model, UI, and tests

The model, UI component, and tests currently use the misspelled ImgagesCount. This will never pick up the real image count (e.g. if the API JSON key is ImagesCount). Please update all occurrences:

Model
File: src/models/v4/calls/callResultData.ts

 export class CallResultData {
-    public ImgagesCount: number = 0;
+    public ImagesCount: number = 0;
   // …other fields
 }

UI component
File: src/app/call/[id].tsx (around lines 492–495)

-            {call?.ImgagesCount ? (
+            {call?.ImagesCount ? (
             <Box className="absolute -right-1 -top-1 h-5 min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1">
-                <Text className="text-xs font-medium text-white">{call.ImgagesCount}</Text>
+                <Text className="text-xs font-medium text-white">{call.ImagesCount}</Text>
             </Box>
           ) : null}

Tests
File: src/app/call/__tests__/[id].test.tsx

-    ImgagesCount: 1,
+    ImagesCount: 1,

Please run a global search/replace for ImgagesCountImagesCount to ensure no leftovers.

env.js (1)

9-9: Align import convention for Env
Update the top‐of‐file docs in env.js to match the named‐import style used throughout the codebase.

• File: env.js (around line 9)
Change the example from a default to a named import:

- * To access the client environment variables in your `src` folder, you can import them from `@env`. For example: `import Env from '@env'`.
+ * To access the client environment variables in your `src` folder, you can import them from `@env`. For example: `import { Env } from '@env'`.
src/services/push-notification.ts (3)

146-211: Harden registration: redact token in logs, simplify token extraction, and add unitId guard

  • Don’t log raw push tokens. They’re sensitive and can be abused.
  • getDevicePushTokenAsync yields the string token in data for both platforms; the ternary is redundant.
  • If unitId or departmentCode is empty, short-circuit to avoid bad backend registrations.

Apply:

   public async registerForPushNotifications(unitId: string, departmentCode: string): Promise<string | null> {
+    if (!unitId || !departmentCode) {
+      logger.warn({
+        message: 'Missing unitId or departmentCode; skipping push registration',
+        context: { hasUnitId: !!unitId, hasDepartmentCode: !!departmentCode },
+      });
+      return null;
+    }
@@
-      // The token format depends on the platform
-      const token = Platform.OS === 'ios' ? devicePushToken.data : devicePushToken.data;
+      // Unified token extraction for both platforms
+      const token = devicePushToken.data as string;
@@
-      logger.info({
+      const redacted = typeof this.pushToken === 'string'
+        ? `${this.pushToken.slice(0, 6)}...${this.pushToken.slice(-4)}`
+        : 'unknown';
+      logger.info({
         message: 'Push notification token obtained',
         context: {
-          token: this.pushToken,
+          token: redacted,
           unitId,
           platform: Platform.OS,
         },
       });

283-309: The usePushNotifications hook is registering with the user’s auth ID instead of the current unit’s ID—this will mis-associate device tokens. You’ll need to pull in your app’s active-unit selector (e.g. useCoreStore((s) => s.activeUnitId)) and switch every userId to unitId.

• File to update: src/services/push-notification.ts
• Lines 283–309: replace const userId = … with const unitId = useCoreStore(…), adjust the effect’s guard to if (rights?.DepartmentCode && unitId && unitId !== previousUnitIdRef.current), call .registerForPushNotifications(unitId, rights.DepartmentCode), log { unitId }, and set previousUnitIdRef.current = unitId.

Diff sketch:

-import { useAuthStore } from '@/stores/auth';
+import { useAuthStore } from '@/stores/auth';
 import { securityStore } from '@/stores/security';
+import { useCoreStore } from '@/stores/core';-export const usePushNotifications = () => {
-  const userId = useAuthStore((state) => state.userId);
+export const usePushNotifications = () => {
+  const unitId = useCoreStore((state) => state.activeUnitId);
   const rights = securityStore((state) => state.rights);
-  const previousUnitIdRef = useRef<string | null>(null);
+  const previousUnitIdRef = useRef<string | null>(null);
 
   useEffect(() => {
-    if (rights && userId && userId !== previousUnitIdRef.current) {
-      pushNotificationService
-        .registerForPushNotifications(userId, rights.DepartmentCode)
+    if (rights?.DepartmentCode && unitId && unitId !== previousUnitIdRef.current) {
+      pushNotificationService
+        .registerForPushNotifications(unitId, rights.DepartmentCode)
         .then((token) => {
           if (token) {
             logger.info({
               message: 'Successfully registered for push notifications',
-              context: { userId: userId },
+              context: { unitId },
             });
           }
         })
@@
-      previousUnitIdRef.current = userId;
+      previousUnitIdRef.current = unitId;
     }
   }, [rights, unitId]);

Run this to find your core‐store selector key if it’s named differently:

rg -n -C3 'activeUnitId' src/stores

22-30: Remove unsupported NotificationBehavior properties for [email protected]

The project’s installed version (expo-notifications 0.29.14) only defines these keys in NotificationBehavior:

  • shouldShowAlert
  • shouldPlaySound
  • shouldSetBadge

Properties shouldShowBanner and shouldShowList aren’t present until a much newer SDK (v53+) and will be ignored (or cause type errors). Either remove them or upgrade your expo-notifications dependency.

Please update src/services/push-notification.ts:

 Notifications.setNotificationHandler({
   handleNotification: async () => ({
     shouldShowAlert: true,
     shouldPlaySound: true,
     shouldSetBadge: true,
-    shouldShowBanner: true,
-    shouldShowList: true,
   }),
 });

If you need banner/list presentation on iOS, consider bumping to expo-notifications >= 0.53.

src/stores/app/__tests__/core-store.test.ts (1)

18-21: Mock path is outdated; tests won’t stub getConfig

You now import getConfig from '@/api/config' (Line 74) but still mock '@/api/config/config'. This leaves getConfig unmocked and will break tests.

Apply this diff:

-jest.mock('@/api/config/config', () => ({
-  getConfig: jest.fn(),
-}));
+jest.mock('@/api/config', () => ({
+  getConfig: jest.fn(),
+}));

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove BIND_TELECOM_CONNECTION_SERVICE from uses-permission

This is a signature-level permission; third-party apps cannot be granted it. Keep it only as the service’s required permission.

-  <uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
📝 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
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
🤖 Prompt for AI Agents
In android/app/src/main/AndroidManifest.xml around line 8, the manifest
incorrectly declares android.permission.BIND_TELECOM_CONNECTION_SERVICE as a
uses-permission (a signature-level permission third-party apps cannot hold);
remove that <uses-permission> entry and ensure the permission remains only as
the service’s requiredPermission attribute on the corresponding <service>
element (i.e., keep
requiredPermission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" on the
telecom connection service declaration).

Comment on lines +64 to 66
version: packageJSON.version,
versionCode: parseInt(packageJSON.versionCode),
adaptiveIcon: {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against NaN for Android versionCode

parseInt(packageJSON.versionCode) will produce NaN if versionCode is missing or not a numeric string. That will break EAS/Gradle config.

Apply this diff to harden parsing and set a sane fallback:

-    versionCode: parseInt(packageJSON.versionCode),
+    versionCode: Number.isInteger(Number(packageJSON.versionCode))
+      ? parseInt(String(packageJSON.versionCode), 10)
+      : 1,
🤖 Prompt for AI Agents
In app.config.ts around lines 64-66, the versionCode currently uses
parseInt(packageJSON.versionCode) which can produce NaN if versionCode is
missing or non-numeric; update it to parse with an explicit radix (parseInt(...,
10)) and guard the result with Number.isFinite/Number.isNaN (or isNaN) to
provide a sane numeric fallback (e.g., 1) so versionCode always resolves to a
valid number for EAS/Gradle.

Comment on lines +143 to 146
"react-native-ble-manager": "^12.1.5",
"react-native-calendars": "^1.1313.0",
"react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971",
"react-native-edge-to-edge": "~1.1.2",
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

BLE stack migration: ensure no remaining ble-plx references and that BleManager is correctly imported

The dependency switch is correct. Make sure the codebase and mocks no longer import react-native-ble-plx, and that all BLE code now uses react-native-ble-manager patterns.

Run:


🏁 Script executed:

#!/bin/bash
# Confirm no residual `react-native-ble-plx` references
rg -n -C2 -P "react-native-ble-plx|BleManager" --type ts --type tsx --type js

# If iOS/Android native setup exists, ensure podspec/gradle steps aren't referencing ble-plx names
rg -n -C2 -P "ble-plx" -g "ios/**" -g "android/**"

Length of output: 307


I’ve refined the search to avoid unrecognized types and to target both JS/TS code and native config files. Please re-run:


🏁 Script executed:

#!/bin/bash
# Search for any remaining imports of react-native-ble-plx
rg -n -C2 "react-native-ble-plx" -g "*.js" -g "*.jsx" -g "*.ts" -g "*.tsx"

# Search for BleManager references (likely from ble-plx)
rg -n -C2 "BleManager" -g "*.js" -g "*.jsx" -g "*.ts" -g "*.tsx"

# Check iOS native files (Podfile and podspecs) for ble-plx mentions
rg -n -C2 "ble-plx" -g "ios/Podfile" -g "ios/**/*.podspec"

# Check Android Gradle config for ble-plx mentions
rg -n -C2 "ble-plx" -g "android/**/*.gradle" -g "android/**/build.gradle"

Length of output: 9683


Update outdated BLE comment reference

Our search confirms all react-native-ble-plx imports have been removed and no native (iOS/Android) configs reference “ble-plx.” However, there’s still one outdated comment:

  • src/stores/app/bluetooth-audio-store.ts, line 7
    Change
    // Bluetooth state enum to match react-native-ble-plx API
    to reference react-native-ble-manager instead of react-native-ble-plx.
🤖 Prompt for AI Agents
In src/stores/app/bluetooth-audio-store.ts around line 7, update the comment
that currently reads "// Bluetooth state enum to match react-native-ble-plx API"
to reference the correct library: replace "react-native-ble-plx" with
"react-native-ble-manager" so the comment becomes something like "// Bluetooth
state enum to match react-native-ble-manager API"; keep the rest of the comment
intact.

Comment on lines 346 to 351
// Keep processing in background for a short time
setTimeout(() => {
if (AppState.currentState === 'background') {
this.stopProcessing();
}
}, 30000); // 30 seconds
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential memory leak with background timeout.

The timeout created when the app goes to background might not be cleared if the app state changes before the timeout fires, potentially causing a memory leak or unexpected behavior.

+  private backgroundTimeoutId: NodeJS.Timeout | null = null;
+
   private handleAppStateChange = (nextAppState: AppStateStatus): void => {
     logger.info({
       message: 'Offline event manager handling app state change',
       context: { nextAppState },
     });
 
+    // Clear any existing background timeout
+    if (this.backgroundTimeoutId) {
+      clearTimeout(this.backgroundTimeoutId);
+      this.backgroundTimeoutId = null;
+    }
+
     if (nextAppState === 'active') {
       this.startProcessing();
     } else if (nextAppState === 'background') {
       // Keep processing in background for a short time
-      setTimeout(() => {
+      this.backgroundTimeoutId = setTimeout(() => {
         if (AppState.currentState === 'background') {
           this.stopProcessing();
         }
+        this.backgroundTimeoutId = null;
       }, 30000); // 30 seconds
     } else if (nextAppState === 'inactive') {
       this.stopProcessing();
     }
   };

Also update the cleanup method:

   public cleanup(): void {
     this.stopProcessing();
 
+    if (this.backgroundTimeoutId) {
+      clearTimeout(this.backgroundTimeoutId);
+      this.backgroundTimeoutId = null;
+    }
+
     if (this.appStateSubscription) {
       this.appStateSubscription.remove();
       this.appStateSubscription = null;
     }
🤖 Prompt for AI Agents
In src/services/offline-event-manager.service.ts around lines 346 to 351, the
setTimeout created when the app enters background is never tracked or cleared
which can leak if app state changes before it fires; store the timeout id (typed
appropriately for browser/Node), replace the inline setTimeout with assignment
to a class property, clear it with clearTimeout when the app state changes or
when stopping processing, and update the cleanup method to clear and null out
that timeout id to avoid lingering timers.

Comment on lines 120 to 123
await TaskManager.defineTask(QUEUE_PROCESSOR_TASK_NAME, async () => {
await this.processQueue();
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix duplicate task definition.

The task is being defined twice - once in startBackgroundProcessing (lines 120-122) and already at the module level (lines 14-28). This duplicate definition is unnecessary and may cause unexpected behavior.

       // Register background task if not already registered
       const isTaskRegistered = await TaskManager.isTaskRegisteredAsync(QUEUE_PROCESSOR_TASK_NAME);
       if (!isTaskRegistered) {
-        await TaskManager.defineTask(QUEUE_PROCESSOR_TASK_NAME, async () => {
-          await this.processQueue();
-        });
-
         logger.info({
-          message: 'Background queue processor task registered',
+          message: 'Background queue processor task was not registered - this should not happen',
         });
       }
📝 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
await TaskManager.defineTask(QUEUE_PROCESSOR_TASK_NAME, async () => {
await this.processQueue();
});
// Register background task if not already registered
const isTaskRegistered = await TaskManager.isTaskRegisteredAsync(QUEUE_PROCESSOR_TASK_NAME);
if (!isTaskRegistered) {
logger.info({
message: 'Background queue processor task was not registered - this should not happen',
});
}
🤖 Prompt for AI Agents
In src/services/offline-queue-processor.ts around lines 120 to 123, there is a
duplicate TaskManager.defineTask call that defines the same
QUEUE_PROCESSOR_TASK_NAME already defined at module level (lines 14-28); remove
the second definition inside startBackgroundProcessing (or replace it with a
guard that checks TaskManager.hasTask/exists before defining) so the task is
defined only once and startBackgroundProcessing only starts/enables the existing
task rather than redefining it.

Comment on lines 335 to 339
TaskManager.isTaskRegisteredAsync(QUEUE_PROCESSOR_TASK_NAME).then((isRegistered) => {
if (isRegistered) {
TaskManager.unregisterTaskAsync(QUEUE_PROCESSOR_TASK_NAME);
}
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for background task unregistration.

The background task unregistration lacks error handling. If unregistration fails, it could leave the task registered and potentially cause issues on the next app launch.

     // Unregister background task
-    TaskManager.isTaskRegisteredAsync(QUEUE_PROCESSOR_TASK_NAME).then((isRegistered) => {
-      if (isRegistered) {
-        TaskManager.unregisterTaskAsync(QUEUE_PROCESSOR_TASK_NAME);
-      }
-    });
+    TaskManager.isTaskRegisteredAsync(QUEUE_PROCESSOR_TASK_NAME)
+      .then(async (isRegistered) => {
+        if (isRegistered) {
+          try {
+            await TaskManager.unregisterTaskAsync(QUEUE_PROCESSOR_TASK_NAME);
+            logger.info({
+              message: 'Background task unregistered successfully',
+            });
+          } catch (error) {
+            logger.error({
+              message: 'Failed to unregister background task',
+              context: { error },
+            });
+          }
+        }
+      })
+      .catch((error) => {
+        logger.error({
+          message: 'Failed to check if background task is registered',
+          context: { error },
+        });
+      });
🤖 Prompt for AI Agents
In src/services/offline-queue-processor.ts around lines 335 to 339, the call
that checks and unregisters the background task runs without error handling;
change it to an awaited async flow (or wrap in an immediately-invoked async
function) that first awaits TaskManager.isTaskRegisteredAsync, then if true
awaits TaskManager.unregisterTaskAsync inside a try/catch, and on error logs the
failure (include task name and error) to the module logger/telemetry and
optionally surface or rethrow the error depending on caller expectations; ensure
any Promise rejections are handled so unregistration failures do not go
unobserved.

Comment on lines 6 to 16
export class OfflineQueueService {
private static instance: OfflineQueueService;

private constructor() {}

public static getInstance(): OfflineQueueService {
if (!OfflineQueueService.instance) {
OfflineQueueService.instance = new OfflineQueueService();
}
return OfflineQueueService.instance;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make initialization idempotent

If initialize() is called multiple times (e.g., layout re-mounts), you may start multiple processors or re-wire listeners.

Apply this diff to add a guard flag:

 export class OfflineQueueService {
   private static instance: OfflineQueueService;
+  private initialized = false;

   private constructor() {}
📝 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
export class OfflineQueueService {
private static instance: OfflineQueueService;
private constructor() {}
public static getInstance(): OfflineQueueService {
if (!OfflineQueueService.instance) {
OfflineQueueService.instance = new OfflineQueueService();
}
return OfflineQueueService.instance;
}
export class OfflineQueueService {
private static instance: OfflineQueueService;
private initialized = false;
private constructor() {}
public static getInstance(): OfflineQueueService {
if (!OfflineQueueService.instance) {
OfflineQueueService.instance = new OfflineQueueService();
}
return OfflineQueueService.instance;
}
// Example initialize method using the guard:
public initialize(): void {
if (this.initialized) {
return;
}
this.initialized = true;
// ... set up processors, listeners, etc.
}
}
🤖 Prompt for AI Agents
In src/services/offline-queue.service.ts around lines 6 to 16, the class lacks
an idempotent initialize guard so repeated initialize() calls can start
duplicate processors or listeners; add a private boolean flag (e.g., initialized
= false) to the class, have initialize() check the flag and return immediately
if true, set the flag to true after successful initialization, and optionally
reset it to false in any teardown/destroy method so re-initialization after
cleanup remains possible.

Comment on lines 18 to 41
public async initialize(): Promise<void> {
try {
logger.info({
message: 'Initializing offline queue service',
});

// Initialize the network listener in the store
const queueStore = useOfflineQueueStore.getState();
queueStore.initializeNetworkListener();

// Start the queue processor
await offlineQueueProcessor.startProcessing();

logger.info({
message: 'Offline queue service initialized successfully',
});
} catch (error) {
logger.error({
message: 'Failed to initialize offline queue service',
context: { error },
});
throw error;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard repeated initialize calls and await potential async store setup

Add an early return when already initialized, and set the flag upon success. Also safely await initializeNetworkListener if it becomes async.

Apply this diff:

   public async initialize(): Promise<void> {
     try {
+      if (this.initialized) {
+        logger.info({ message: 'Offline queue service already initialized, skipping' });
+        return;
+      }
       logger.info({
         message: 'Initializing offline queue service',
       });

       // Initialize the network listener in the store
       const queueStore = useOfflineQueueStore.getState();
-      queueStore.initializeNetworkListener();
+      await Promise.resolve(queueStore.initializeNetworkListener?.());

       // Start the queue processor
       await offlineQueueProcessor.startProcessing();

       logger.info({
         message: 'Offline queue service initialized successfully',
       });
+      this.initialized = true;
     } catch (error) {
       logger.error({
         message: 'Failed to initialize offline queue service',
         context: { error },
       });
+      this.initialized = false;
       throw error;
     }
   }
📝 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
public async initialize(): Promise<void> {
try {
logger.info({
message: 'Initializing offline queue service',
});
// Initialize the network listener in the store
const queueStore = useOfflineQueueStore.getState();
queueStore.initializeNetworkListener();
// Start the queue processor
await offlineQueueProcessor.startProcessing();
logger.info({
message: 'Offline queue service initialized successfully',
});
} catch (error) {
logger.error({
message: 'Failed to initialize offline queue service',
context: { error },
});
throw error;
}
}
public async initialize(): Promise<void> {
try {
if (this.initialized) {
logger.info({ message: 'Offline queue service already initialized, skipping' });
return;
}
logger.info({
message: 'Initializing offline queue service',
});
// Initialize the network listener in the store
const queueStore = useOfflineQueueStore.getState();
await Promise.resolve(queueStore.initializeNetworkListener?.());
// Start the queue processor
await offlineQueueProcessor.startProcessing();
logger.info({
message: 'Offline queue service initialized successfully',
});
this.initialized = true;
} catch (error) {
logger.error({
message: 'Failed to initialize offline queue service',
context: { error },
});
this.initialized = false;
throw error;
}
}
🤖 Prompt for AI Agents
In src/services/offline-queue.service.ts around lines 18 to 41, the initialize
method needs a guard against repeated calls and should await the store's network
listener if it becomes async; add an early return when an "initialized" flag is
already true, set that flag to true only after successful startup, and change
the call to queueStore.initializeNetworkListener() to await it (i.e., await
queueStore.initializeNetworkListener()) so the method supports async setup;
ensure the flag is set after offlineQueueProcessor.startProcessing() completes
and clear or handle it on failure so repeated attempts behave correctly.

Comment on lines +283 to +294
run: |
set -eo pipefail
# Grab lines after "## Release Notes" until the next header
RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
# Use a unique delimiter to write multiline into GITHUB_ENV
delimiter="EOF_$(date +%s)_$RANDOM"
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"

Check failure

Code scanning / CodeQL

Environment variable built from user-controlled sources Critical

Potential environment variable injection in [set -eo pipefail Grab lines after "## Release Notes" until the next header RELEASE_NOTES="$(printf '%s\\n' "$PR_BODY" \\
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')" Use a unique delimiter to write multiline into GITHUB_ENV delimiter="EOF_$(date +%s)_$RANDOM"
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"](1), which may be controlled by an external user (
workflow_dispatch
).

Copilot Autofix

AI 5 months ago

To fix the problem, we must ensure that user-controlled data (the PR body) cannot inject new environment variables or break out of the intended assignment in the $GITHUB_ENV file. The best way to do this is:

  • Check that the chosen delimiter does not appear in the extracted release notes. If it does, generate a new delimiter and try again, or sanitize the release notes to remove or escape the delimiter.
  • Alternatively, use a cryptographically strong random delimiter (e.g., using uuidgen if available) to make delimiter collision practically impossible.
  • Optionally, sanitize the release notes to remove any lines that could be interpreted as environment variable assignments, or to remove newlines if a single-line variable is sufficient.

In this case, the best fix is to use uuidgen to generate a strong, unique delimiter, and to check that the delimiter does not appear in the release notes before writing to $GITHUB_ENV. If uuidgen is not available, fall back to the current method, but still check for delimiter collisions.

Required changes:

  • In the step at lines 279-295, update the delimiter generation to use uuidgen if available.
  • Add a check to ensure the delimiter does not appear in the release notes. If it does, regenerate the delimiter.
  • Optionally, add a maximum number of attempts to avoid infinite loops.

Suggested changeset 1
.github/workflows/react-native-cicd.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/react-native-cicd.yml b/.github/workflows/react-native-cicd.yml
--- a/.github/workflows/react-native-cicd.yml
+++ b/.github/workflows/react-native-cicd.yml
@@ -285,14 +285,29 @@
           # Grab lines after "## Release Notes" until the next header
           RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
             | awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
-          # Use a unique delimiter to write multiline into GITHUB_ENV
-          delimiter="EOF_$(date +%s)_$RANDOM"
+          # Use a cryptographically strong unique delimiter and check for collision
+          max_attempts=5
+          attempt=0
+          while [ $attempt -lt $max_attempts ]; do
+            if command -v uuidgen >/dev/null 2>&1; then
+              delimiter="EOF_$(uuidgen)"
+            else
+              delimiter="EOF_$(date +%s)_$RANDOM"
+            fi
+            if ! printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}" | grep -q "$delimiter"; then
+              break
+            fi
+            attempt=$((attempt+1))
+          done
+          if [ $attempt -eq $max_attempts ]; then
+            echo "Failed to generate a safe delimiter for RELEASE_NOTES" >&2
+            exit 1
+          fi
           {
             echo "RELEASE_NOTES<<$delimiter"
             printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
             echo "$delimiter"
           } >> "$GITHUB_ENV"
-
       - name: 📋 Prepare Release Notes file
         if: ${{ matrix.platform == 'android' }}
         run: |
EOF
@@ -285,14 +285,29 @@
# Grab lines after "## Release Notes" until the next header
RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
# Use a unique delimiter to write multiline into GITHUB_ENV
delimiter="EOF_$(date +%s)_$RANDOM"
# Use a cryptographically strong unique delimiter and check for collision
max_attempts=5
attempt=0
while [ $attempt -lt $max_attempts ]; do
if command -v uuidgen >/dev/null 2>&1; then
delimiter="EOF_$(uuidgen)"
else
delimiter="EOF_$(date +%s)_$RANDOM"
fi
if ! printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}" | grep -q "$delimiter"; then
break
fi
attempt=$((attempt+1))
done
if [ $attempt -eq $max_attempts ]; then
echo "Failed to generate a safe delimiter for RELEASE_NOTES" >&2
exit 1
fi
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"

- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
run: |
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
// Mock analytics hook
jest.mock('@/hooks/use-analytics');
const mockUseAnalytics = useAnalytics as jest.MockedFunction<typeof useAnalytics>;

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High test

This string may still contain
<script
, which may cause an HTML element injection vulnerability.

Copilot Autofix

AI 5 months ago

To fix the incomplete multi-character sanitization, we should ensure that the mock stripHtmlTags function removes all HTML tags robustly, including script tags and edge cases. The best way is to use a well-tested library such as sanitize-html for sanitization, even in mocks. If adding a dependency is not desirable in test code, we can apply the regular expression replacement repeatedly until no more replacements are made, as recommended in the background. This ensures that all tags are removed, even if nested or malformed.

Specifically, in src/components/protocols/__tests__/protocol-details-sheet.test.tsx, update the mock implementation of stripHtmlTags (line 17) to repeatedly apply the replacement until the string no longer changes. No new imports are needed, as this is a simple function.


Suggested changeset 1
src/components/protocols/__tests__/protocol-details-sheet.test.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
--- a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
+++ b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
@@ -14,7 +14,15 @@
 jest.mock('@/lib/utils', () => ({
   formatDateForDisplay: jest.fn((date) => date ? '2023-01-01 12:00 UTC' : ''),
   parseDateISOString: jest.fn((dateString) => dateString ? new Date(dateString) : null),
-  stripHtmlTags: jest.fn((html) => html ? html.replace(/<[^>]*>/g, '') : ''),
+  stripHtmlTags: jest.fn((html) => {
+    if (!html) return '';
+    let previous;
+    do {
+      previous = html;
+      html = html.replace(/<[^>]*>/g, '');
+    } while (html !== previous);
+    return html;
+  }),
 }));
 
 jest.mock('nativewind', () => ({
EOF
@@ -14,7 +14,15 @@
jest.mock('@/lib/utils', () => ({
formatDateForDisplay: jest.fn((date) => date ? '2023-01-01 12:00 UTC' : ''),
parseDateISOString: jest.fn((dateString) => dateString ? new Date(dateString) : null),
stripHtmlTags: jest.fn((html) => html ? html.replace(/<[^>]*>/g, '') : ''),
stripHtmlTags: jest.fn((html) => {
if (!html) return '';
let previous;
do {
previous = html;
html = html.replace(/<[^>]*>/g, '');
} while (html !== previous);
return html;
}),
}));

jest.mock('nativewind', () => ({
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated

try {
// Generate a new UUID for this call
currentCallUUID = this.generateUUID();

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 5 months ago

To fix the problem, we should ensure that the fallback UUID generation does not use Math.random(), but instead uses a cryptographically secure random number generator. In a React Native environment, the best approach is to use the react-native-get-random-values polyfill (if available) or Node's crypto module for secure random values. Since we cannot assume the presence of external libraries not shown in the code, and we are limited to well-known APIs, we should use the Web Crypto API (crypto.getRandomValues) if available, or Node's crypto.randomBytes if running in a Node environment. The fallback should generate random bytes and use them to construct the UUID, following the RFC4122 version 4 specification.

The changes should be made in the generateUUID() method in src/services/callkeep.service.ios.ts. We need to:

  • Add a secure fallback for UUID generation using crypto.getRandomValues (browser/React Native) or require('crypto').randomBytes (Node).
  • Avoid using Math.random() for any part of the UUID generation.
Suggested changeset 1
src/services/callkeep.service.ios.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/callkeep.service.ios.ts b/src/services/callkeep.service.ios.ts
--- a/src/services/callkeep.service.ios.ts
+++ b/src/services/callkeep.service.ios.ts
@@ -295,12 +295,44 @@
     // RN 0.76 typically provides global crypto.randomUUID via Hermes/JSI
     const rndUUID = (global as any)?.crypto?.randomUUID?.();
     if (typeof rndUUID === 'string') return rndUUID;
-    // Fallback
-    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
-      const r = (Math.random() * 16) | 0;
-      const v = c === 'x' ? r : (r & 0x3) | 0x8;
-      return v.toString(16);
-    });
+    // Fallback: Use cryptographically secure random values
+    let getRandomValues: ((arr: Uint8Array) => Uint8Array) | null = null;
+    if (typeof global !== 'undefined' && global.crypto && typeof global.crypto.getRandomValues === 'function') {
+      getRandomValues = global.crypto.getRandomValues.bind(global.crypto);
+    } else if (typeof require === 'function') {
+      try {
+        // Node.js environment
+        const nodeCrypto = require('crypto');
+        getRandomValues = (arr: Uint8Array) => {
+          const buf = nodeCrypto.randomBytes(arr.length);
+          arr.set(buf);
+          return arr;
+        };
+      } catch (e) {
+        // No secure random available
+        getRandomValues = null;
+      }
+    }
+    if (getRandomValues) {
+      // Generate RFC4122 version 4 UUID
+      const bytes = getRandomValues(new Uint8Array(16));
+      // Set version bits (4) and variant bits (RFC4122)
+      bytes[6] = (bytes[6] & 0x0f) | 0x40;
+      bytes[8] = (bytes[8] & 0x3f) | 0x80;
+      const hex: string[] = [];
+      for (let i = 0; i < bytes.length; i++) {
+        hex.push(bytes[i].toString(16).padStart(2, '0'));
+      }
+      return (
+        hex.slice(0, 4).join('') + '-' +
+        hex.slice(4, 6).join('') + '-' +
+        hex.slice(6, 8).join('') + '-' +
+        hex.slice(8, 10).join('') + '-' +
+        hex.slice(10, 16).join('')
+      );
+    }
+    // If no secure random available, throw error
+    throw new Error('No cryptographically secure random number generator available for UUID generation');
   }
 
   /**
EOF
@@ -295,12 +295,44 @@
// RN 0.76 typically provides global crypto.randomUUID via Hermes/JSI
const rndUUID = (global as any)?.crypto?.randomUUID?.();
if (typeof rndUUID === 'string') return rndUUID;
// Fallback
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
// Fallback: Use cryptographically secure random values
let getRandomValues: ((arr: Uint8Array) => Uint8Array) | null = null;
if (typeof global !== 'undefined' && global.crypto && typeof global.crypto.getRandomValues === 'function') {
getRandomValues = global.crypto.getRandomValues.bind(global.crypto);
} else if (typeof require === 'function') {
try {
// Node.js environment
const nodeCrypto = require('crypto');
getRandomValues = (arr: Uint8Array) => {
const buf = nodeCrypto.randomBytes(arr.length);
arr.set(buf);
return arr;
};
} catch (e) {
// No secure random available
getRandomValues = null;
}
}
if (getRandomValues) {
// Generate RFC4122 version 4 UUID
const bytes = getRandomValues(new Uint8Array(16));
// Set version bits (4) and variant bits (RFC4122)
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex: string[] = [];
for (let i = 0; i < bytes.length; i++) {
hex.push(bytes[i].toString(16).padStart(2, '0'));
}
return (
hex.slice(0, 4).join('') + '-' +
hex.slice(4, 6).join('') + '-' +
hex.slice(6, 8).join('') + '-' +
hex.slice(8, 10).join('') + '-' +
hex.slice(10, 16).join('')
);
}
// If no secure random available, throw error
throw new Error('No cryptographically secure random number generator available for UUID generation');
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 90

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (16)
docs/compose-message-sheet-analytics-implementation.md (1)

137-153: Remove PII: raw recipient IDs must not be sent

We’ve confirmed that all 8 analytics events are implemented and exercised by tests. However, the payload for the compose_message_recipient_toggled event still includes a raw recipientId, which exposes user‐identifiable information. Please remove or anonymize this field.

• In src/components/messages/compose-message-sheet.tsx, lines 227–231 (inside the trackEvent('compose_message_recipient_toggled', { … }) call), remove or replace:

  trackEvent('compose_message_recipient_toggled', {
    timestamp: new Date().toISOString(),
-     recipientId: id,
+     // recipientId: id,  ← remove or replace with hashed/pseudonymous ID or count
  });

Consider sending a hashed/pseudonymous identifier or simply the total count of selected recipients instead.

src/app/(app)/shifts.tsx (2)

54-57: Prevent crash when filtering by code: guard against undefined/null Code

shift.Code.toLowerCase() will throw if Code is null/undefined. Harden the filter to avoid runtime crashes with real backend data.

Apply this diff:

-    if (!searchQuery.trim()) return shifts;
-    const query = searchQuery.trim().toLowerCase();
-    return shifts.filter((shift) => shift.Name.toLowerCase().includes(query) || shift.Code.toLowerCase().includes(query));
+    const q = searchQuery.trim().toLowerCase();
+    if (!q) return shifts;
+    return shifts.filter((shift) => {
+      const name = (shift.Name || '').toLowerCase();
+      const code = (shift.Code || '').toLowerCase();
+      return name.includes(q) || code.includes(q);
+    });

59-63: Defensive filter for today’s shifts: guard ShiftName access

shiftDay.ShiftName.toLowerCase() will throw if ShiftName is null/undefined. Add a safe fallback.

Apply this diff:

-    if (!searchQuery.trim()) return todaysShiftDays;
-    const query = searchQuery.trim().toLowerCase();
-    return todaysShiftDays.filter((shiftDay) => shiftDay.ShiftName.toLowerCase().includes(query));
+    const q = searchQuery.trim().toLowerCase();
+    if (!q) return todaysShiftDays;
+    return todaysShiftDays.filter((shiftDay) => (shiftDay.ShiftName || '').toLowerCase().includes(q));
src/app/(app)/__tests__/shifts.test.tsx (1)

143-155: Incorrect mock target: mocking zero-state-example instead of zero-state

Production imports @/components/common/zero-state, but the test mocks zero-state-example. This likely breaks zero-state assertions (getByTestId('zero-state')) or makes tests rely on the real component.

Apply this diff:

-jest.mock('@/components/common/zero-state-example', () => {
+jest.mock('@/components/common/zero-state', () => {
   const React = require('react');
   const { View, Text } = require('react-native');
   return {
     __esModule: true,
     default: ({ title }: any) =>
       React.createElement(
         View,
         { testID: 'zero-state' },
         React.createElement(Text, null, title)
       ),
   };
 });
src/app/onboarding.tsx (2)

66-72: Prevent analytics failures from crashing the UI (wrap trackEvent in a safe helper).

If trackEvent throws (network, provider init, etc.), the screen can crash (your tests even simulate this). Add a small safe wrapper and reuse it for all emissions.

Apply this diff to introduce a safe wrapper and strengthen types on refs:

-  const { trackEvent } = useAnalytics();
-  const router = useRouter();
-  const [currentIndex, setCurrentIndex] = useState(0);
-  const flatListRef = useRef<FlatList>(null);
-  const buttonOpacity = useSharedValue(0);
-  const { colorScheme } = useColorScheme();
+  const { trackEvent } = useAnalytics();
+  const safeTrack = useCallback(
+    (name: string, props: Record<string, any>) => {
+      try {
+        trackEvent(name, props);
+      } catch (_err) {
+        // Swallow analytics errors to avoid affecting the UX
+      }
+    },
+    [trackEvent]
+  );
+  const router = useRouter();
+  const [currentIndex, setCurrentIndex] = useState(0);
+  const flatListRef = useRef<FlatList<OnboardingItemProps>>(null);
+  const lastIndexRef = useRef(0);
+  const buttonOpacity = useSharedValue(0);
+  const { colorScheme } = useColorScheme();

88-109: Deduplicate slide-change analytics; current state-based guard can double-fire during fast scroll.

Using currentIndex inside the scroll handler can emit multiple identical events before state updates. Track the last sent index via a ref and only emit when the index actually changes. Also, use the proper RN scroll event type.

-  const handleScroll = (event: { nativeEvent: { contentOffset: { x: number } } }) => {
-    const index = Math.round(event.nativeEvent.contentOffset.x / width);
-    const wasLastIndex = currentIndex;
-    setCurrentIndex(index);
-
-    // Analytics: Track slide changes
-    if (index !== wasLastIndex) {
-      trackEvent('onboarding_slide_changed', {
-        timestamp: new Date().toISOString(),
-        fromSlide: wasLastIndex,
-        toSlide: index,
-        slideTitle: onboardingData[index]?.title || 'Unknown',
-      });
-    }
+  const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
+    const index = Math.round(event.nativeEvent.contentOffset.x / width);
+    const from = lastIndexRef.current;
+    if (index !== from) {
+      lastIndexRef.current = index;
+      setCurrentIndex(index);
+      // Analytics: Track slide changes
+      safeTrack('onboarding_slide_changed', {
+        timestamp: new Date().toISOString(),
+        fromSlide: from,
+        toSlide: index,
+        slideTitle: onboardingData[index]?.title || 'Unknown',
+      });
+    }
 
     // Show button with animation when on the last slide
     if (index === onboardingData.length - 1) {
       buttonOpacity.value = withTiming(1, { duration: 500 });
     } else {
       buttonOpacity.value = withTiming(0, { duration: 300 });
     }
   };

Additionally, update the import to use proper types (outside these lines):

// at top-level imports
import { Dimensions, FlatList, Image, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
.github/workflows/react-native-cicd.yml (1)

191-198: Quote and safely write GRADLE_PROPERTIES to preserve content and avoid word-splitting.

Unquoted variable expansion with echo may mangle whitespace or special characters. Use printf with quotes.

       mkdir -p ~/.gradle/
-      echo ${GRADLE_PROPERTIES} > ~/.gradle/gradle.properties
+      printf '%s' "${GRADLE_PROPERTIES}" > ~/.gradle/gradle.properties
app.config.ts (1)

74-83: Missing Android 12+ Bluetooth permissions; remove unsupported CAPTURE_AUDIO_OUTPUT.

  • BLE on Android 12+ requires BLUETOOTH_CONNECT and BLUETOOTH_SCAN (and optionally BLUETOOTH_ADVERTISE). Without these, scanning/connecting will fail at runtime.
  • android.permission.CAPTURE_AUDIO_OUTPUT is a signature-level permission and cannot be granted to third-party apps; keeping it may cause Play Console warnings/rejections.
     permissions: [
       'android.permission.WAKE_LOCK',
       'android.permission.RECORD_AUDIO',
-      'android.permission.CAPTURE_AUDIO_OUTPUT',
       'android.permission.POST_NOTIFICATIONS',
       'android.permission.FOREGROUND_SERVICE',
       'android.permission.FOREGROUND_SERVICE_MICROPHONE',
       'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE',
       'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
+      // BLE permissions (Android 12+)
+      'android.permission.BLUETOOTH_CONNECT',
+      'android.permission.BLUETOOTH_SCAN',
+      'android.permission.BLUETOOTH_ADVERTISE',
+      // Legacy BLE (pre-Android 12) for broader compatibility
+      'android.permission.BLUETOOTH',
+      'android.permission.BLUETOOTH_ADMIN',
     ],

If location is also used for BLE scanning on older Android, ACCESS_FINE_LOCATION may be needed, but expo-location likely already adds it via its plugin.

src/app/call/[id]/edit.tsx (1)

210-213: Incorrect truthiness check drops valid 0.0 coordinates

Using if (selectedLocation?.latitude && selectedLocation?.longitude) will skip setting coordinates at 0 or 0.0. Use explicit null/undefined checks.

Apply this fix:

-      if (selectedLocation?.latitude && selectedLocation?.longitude) {
-        data.latitude = selectedLocation.latitude;
-        data.longitude = selectedLocation.longitude;
-      }
+      if (
+        selectedLocation &&
+        typeof selectedLocation.latitude === 'number' &&
+        typeof selectedLocation.longitude === 'number'
+      ) {
+        data.latitude = selectedLocation.latitude;
+        data.longitude = selectedLocation.longitude;
+      }
src/app/login/index.tsx (1)

1-117: PII in analytics events — raw username is being sent (action required)

Verified: repo contains production analytics calls that include the raw username and multiple tests that assert that field. This is PII and should not be forwarded to the analytics provider.

Files that need attention

  • src/app/login/index.tsx — trackEvent('login_attempted', { username: data.username }) (production)
  • src/app/(app)/settings.tsx — trackEvent('settings_login_info_updated', { username: data.username }) (production)
  • src/services/aptabase.service.ts — central analytics wrapper (add scrub here if you prefer a single enforcement point)
  • Tests that will need updates if payloads change:
    • src/app/login/tests/index-analytics-simple.test.tsx
    • src/app/login/tests/index-analytics-integration.test.ts
    • src/app/login/tests/index.test.tsx
    • src/components/settings/tests/login-info-bottom-sheet.test.tsx
    • (and other tests that assert username in analytics — search for username in test files)

Recommended fixes (pick one or combine)

  • Remove raw username from analytics payloads. Replace with a non-identifying field, e.g. hasUsername: !!data.username or username_present: true.
  • OR send only a pseudonymous identifier (non-reversible, per-tenant/per-installation ID) if you must correlate users — do not send cleartext username or email.
  • Enforce scrubbing centrally in src/services/aptabase.service.ts (drop known PII keys: username, email, password, token, ssn) before calling the external trackEvent.
  • Update affected tests to assert the new payload shape.

If you want, I can:

  • open a PR that (A) removes username from the two production call sites and (B) adds a PII-stripper in src/services/aptabase.service.ts and updates tests accordingly.
src/app/call/[id].tsx (1)

170-174: Same coordinate falsy-check bug in route guard.

A 0/0.0 coordinate will incorrectly block routing. Use null checks.

-    if (!coordinates.latitude || !coordinates.longitude) {
+    if (coordinates.latitude === null || coordinates.longitude === null) {
       showToast('error', t('call_detail.no_location_for_routing'));
       return;
     }
src/app/(app)/map.tsx (2)

65-105: Prevent multiple map_viewed emissions and unintended resets while focused

The useFocusEffect depends on isMapReady, location.latitude/longitude/isMapLocked/heading, and trackEvent. While focused, any of these changes will re-run the effect, re-emitting map_viewed and re-running “focus-time” reset logic (which can fight the user).

Emit map_viewed only on focus transitions, and decouple the focus reset from dynamic dependencies. Use useIsFocused for the view event and read latest store values at focus time to avoid stale closures.

Apply this diff:

@@
-import { useFocusEffect } from 'expo-router';
+import { useFocusEffect } from 'expo-router';
+import { useIsFocused } from '@react-navigation/native';
@@
   const { t } = useTranslation();
   const { trackEvent } = useAnalytics();
+  const isFocused = useIsFocused();
@@
-  // Handle navigation focus - reset map state when user navigates back to map page
-  useFocusEffect(
-    useCallback(() => {
-      // Track analytics when view becomes visible
-      trackEvent('map_viewed', {
-        timestamp: new Date().toISOString(),
-        isMapLocked: location.isMapLocked,
-        hasLocation: !!(location.latitude && location.longitude),
-      });
-
-      // Reset hasUserMovedMap when navigating back to map
-      setHasUserMovedMap(false);
-
-      // Reset camera to current location when navigating back to map
-      if (isMapReady && location.latitude && location.longitude) {
-        const cameraConfig: any = {
-          centerCoordinate: [location.longitude, location.latitude],
-          zoomLevel: location.isMapLocked ? 16 : 12,
-          animationDuration: 1000,
-          heading: 0,
-          pitch: 0,
-        };
-
-        // Add heading and pitch for navigation mode when locked
-        if (location.isMapLocked && location.heading !== null && location.heading !== undefined) {
-          cameraConfig.heading = location.heading;
-          cameraConfig.pitch = 45;
-        }
-
-        cameraRef.current?.setCamera(cameraConfig);
-
-        logger.info({
-          message: 'Home Map focused, resetting camera to current location',
-          context: {
-            latitude: location.latitude,
-            longitude: location.longitude,
-            isMapLocked: location.isMapLocked,
-          },
-        });
-      }
-    }, [isMapReady, location.latitude, location.longitude, location.isMapLocked, location.heading, trackEvent])
-  );
+  // Emit analytics only when the screen becomes focused (avoid dependency-driven re-fires)
+  useEffect(() => {
+    if (isFocused) {
+      const { latitude, longitude, isMapLocked } = useLocationStore.getState();
+      trackEvent('map_viewed', {
+        timestamp: new Date().toISOString(),
+        isMapLocked,
+        hasLocation: latitude != null && longitude != null,
+      });
+    }
+  }, [isFocused, trackEvent]);
+
+  // Handle navigation focus - reset local map state and camera once on focus
+  useFocusEffect(
+    useCallback(() => {
+      // Reset hasUserMovedMap when navigating back to map
+      setHasUserMovedMap(false);
+
+      // Reset camera to current location when navigating back to map
+      const { latitude, longitude, isMapLocked, heading } = useLocationStore.getState();
+      if (isMapReady && latitude != null && longitude != null) {
+        const cameraConfig: any = {
+          centerCoordinate: [longitude, latitude],
+          zoomLevel: isMapLocked ? 16 : 12,
+          animationDuration: 1000,
+          heading: 0,
+          pitch: 0,
+        };
+
+        // Add heading and pitch for navigation mode when locked
+        if (isMapLocked && heading !== null && heading !== undefined) {
+          cameraConfig.heading = heading;
+          cameraConfig.pitch = 45;
+        }
+
+        cameraRef.current?.setCamera(cameraConfig);
+
+        logger.info({
+          message: 'Home Map focused, resetting camera to current location',
+          context: {
+            latitude,
+            longitude,
+            isMapLocked,
+          },
+        });
+      }
+      return undefined;
+    }, [isMapReady])
+  );

1-489: Use explicit nullish checks for latitude/longitude across the repo

Several components and analytics calls currently use truthiness checks (latitude && longitude or !!(latitude && longitude)) to guard on coordinates. Because 0 is falsy, valid coordinates on the equator or prime meridian would be treated as “no location.” To avoid this bug, replace these with explicit nullish checks (latitude != null && longitude != null or Number.isFinite(latitude) && Number.isFinite(longitude)).

Affected locations include (but may not be limited to):

  • src/components/sidebar/call-sidebar.tsx:79
  • src/api/calls/callNotes.ts:24
  • src/api/calls/callFiles.ts:140
  • src/app/call/[id].tsx:138, 181, 540
  • src/app/call/new/index.tsx:234
  • src/app/(app)/map.tsx:71, 78, 134, 173, 229, 307, 345
  • Various analytics‐integration tests under src/app/call/**/tests

Action items:

  • Update each guard to use latitude != null && longitude != null (or equivalent).
  • Ensure analytics payloads (hasCoordinates, hasLocation, etc.) also use nullish checks.
  • Add or update unit tests to cover edge cases where latitude or longitude is 0.
src/app/(app)/__tests__/map.test.tsx (2)

112-136: Make MapView mock trigger user interaction to deterministically surface the recenter button and event

Right now, MapView is a passive container and never calls onCameraChanged. As a result, hasUserMovedMap never flips to true, so tests that depend on the recenter button being rendered end up conditional or inert. Drive the callback once on mount to simulate a user gesture and make the recenter path testable and deterministic.

Apply this diff to your Mapbox mock:

 jest.mock('@rnmapbox/maps', () => ({
   __esModule: true,
   default: {
     setAccessToken: jest.fn(),
     StyleURL: {
       Street: 'mapbox://styles/mapbox/streets-v11',
       Satellite: 'mapbox://styles/mapbox/satellite-v9',
     },
-    MapView: ({ children, testID }: any) => {
-      const { View } = require('react-native');
-      return <View testID={testID}>{children}</View>;
-    },
+    MapView: (props: any) => {
+      const React = require('react');
+      const { View } = require('react-native');
+      // Simulate a single user interaction after mount
+      React.useEffect(() => {
+        props.onCameraChanged?.({ properties: { isUserInteraction: true } });
+        // Also mark map load complete so isMapReady paths are unblocked
+        props.onDidFinishLoadingMap?.();
+      }, []);
+      return <View testID={props.testID}>{props.children}</View>;
+    },
     Camera: ({ children }: any) => {
       const { View } = require('react-native');
       return <View testID="map-camera">{children}</View>;
     },
     PointAnnotation: ({ children, testID }: any) => {
       const { View } = require('react-native');
       return <View testID={testID || 'point-annotation'}>{children}</View>;
     },
     UserTrackingMode: {
       FollowWithHeading: 'follow-with-heading',
     },
   },
 }));

372-390: Test name vs. assertion mismatch: either rename or assert presence after simulated movement

The test is titled “shows recenter button when user has moved map and location is available” but only asserts that the button is not visible. After enabling simulated user interaction (see MapView mock refactor), you should assert the button appears.

-  it('shows recenter button when user has moved map and location is available', async () => {
+  it('shows recenter button when user has moved map and location is available', async () => {
     const mockLocationStore = jest.requireMock('@/stores/app/location-store') as any;
     mockLocationStore.useLocationStore.mockReturnValue({
       latitude: 40.7128,
       longitude: -74.006,
       heading: 90,
       isMapLocked: false,
     });

     render(<HomeMap />);

     // Simulate map ready
     await waitFor(() => {
       expect(screen.getByTestId('home-map-view')).toBeTruthy();
     });

-    // Initially, recenter button should not be visible
-    expect(screen.queryByTestId('recenter-button')).toBeNull();
+    // After user interaction is simulated by the MapView mock, the recenter button should appear
+    await waitFor(() => {
+      expect(screen.getByTestId('recenter-button')).toBeTruthy();
+    });
   });
src/app/(app)/messages.tsx (1)

69-89: messages_viewed will re-fire whenever the message count changes; scope analytics to focus only

Including filteredMessages.length in the useFocusEffect dependency array will retrigger the focus effect (and the messages_viewed event) when the list changes while the screen is focused, inflating your analytics. If the intent is to track a “screen viewed” event, it should fire on focus only. Consider removing filteredMessages.length from the dependencies (and, if desired, emitting a separate “messages_count_changed” event via a plain useEffect).

   useFocusEffect(
     useCallback(() => {
       // Track analytics when view becomes visible
       trackEvent('messages_viewed', {
         timestamp: new Date().toISOString(),
         currentFilter,
         messageCount: filteredMessages.length,
       });

       if (currentFilter === 'sent') {
         fetchSentMessages();
       } else if (currentFilter === 'inbox') {
         fetchInboxMessages();
       } else {
         // For 'all', fetch both inbox and sent messages
         fetchInboxMessages();
         fetchSentMessages();
       }
-    }, [fetchInboxMessages, fetchSentMessages, currentFilter, trackEvent, filteredMessages.length])
+    }, [fetchInboxMessages, fetchSentMessages, currentFilter, trackEvent])
   );

Optionally, if you want to track list changes independently:

useEffect(() => {
  trackEvent('messages_count_changed', {
    timestamp: new Date().toISOString(),
    currentFilter,
    messageCount: filteredMessages.length,
  });
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredMessages.length]);
♻️ Duplicate comments (2)
.github/workflows/react-native-cicd.yml (1)

157-166: Android versionCode computation and jq update look correct (resolves prior issue).

Using a numeric androidVersionCode and a single jq invocation with --arg/--argjson is the right fix. This addresses the previously flagged non-expanded string and type issue.

app.config.ts (1)

66-66: Guard against NaN/invalid Android versionCode (duplicate of prior feedback).

parseInt(packageJSON.versionCode) can yield NaN or an unintended value if undefined or non-numeric. This will break EAS/Gradle.

Apply this diff:

-    versionCode: parseInt(packageJSON.versionCode),
+    versionCode: Number.isInteger(Number(packageJSON.versionCode))
+      ? parseInt(String(packageJSON.versionCode), 10)
+      : 1,

Comment on lines +279 to +295
- name: 📋 Extract Release Notes from PR Body
if: ${{ matrix.platform == 'android' }}
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -eo pipefail
# Grab lines after "## Release Notes" until the next header
RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
# Use a unique delimiter to write multiline into GITHUB_ENV
delimiter="EOF_$(date +%s)_$RANDOM"
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid writing PR body into GITHUB_ENV; write directly to a file to eliminate env injection risk.

PR bodies are user-controlled on pull_request and workflow_dispatch. Even with a randomized delimiter, writing arbitrary content into GITHUB_ENV is unnecessary here and flagged by CodeQL. Generate the release notes file directly and drop the environment variable.

Apply this diff to remove the env write step:

-      - name: 📋 Extract Release Notes from PR Body
-        if: ${{ matrix.platform == 'android' }}
-        env:
-          PR_BODY: ${{ github.event.pull_request.body }}
-        run: |
-          set -eo pipefail
-          # Grab lines after "## Release Notes" until the next header
-          RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
-            | awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
-          # Use a unique delimiter to write multiline into GITHUB_ENV
-          delimiter="EOF_$(date +%s)_$RANDOM"
-          {
-            echo "RELEASE_NOTES<<$delimiter"
-            printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
-            echo "$delimiter"
-          } >> "$GITHUB_ENV"

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 283-294: Environment variable built from user-controlled sources
Potential environment variable injection in [set -eo pipefail

Grab lines after "## Release Notes" until the next header

RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"

Use a unique delimiter to write multiline into GITHUB_ENV

delimiter="EOF_$(date +%s)_$RANDOM"
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"](1), which may be controlled by an external user (workflow_dispatch).

🤖 Prompt for AI Agents
.github/workflows/react-native-cicd.yml around lines 279-295: the job currently
extracts the "## Release Notes" section from the PR body and writes it into
GITHUB_ENV using a randomized delimiter, which risks env injection; instead,
write the extracted release notes directly to a file in the workspace (for
example, ./release-notes.txt or ./release-notes.md) and remove the GITHUB_ENV
write and delimiter logic; keep the same extraction logic but redirect output to
a file (with a safe default like "No release notes provided." when empty) and
update any downstream steps to read that file rather than relying on an
environment variable.

Comment on lines +296 to +304
- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
run: |
{
echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
echo
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
} > RELEASE_NOTES.md
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Build the RELEASE_NOTES.md directly (no dependency on env state).

Inline the extraction in this step and write the file directly.

-      - name: 📋 Prepare Release Notes file
-        if: ${{ matrix.platform == 'android' }}
-        run: |
-          {
-            echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
-            echo
-            printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
-          } > RELEASE_NOTES.md
+      - name: 📋 Prepare Release Notes file
+        if: ${{ matrix.platform == 'android' }}
+        env:
+          PR_BODY: ${{ github.event.pull_request.body }}
+        run: |
+          set -eo pipefail
+          NOTES="$(printf '%s\n' "$PR_BODY" | awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
+          {
+            echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
+            echo
+            if [ -n "$NOTES" ]; then
+              printf '%s\n' "$NOTES"
+            else
+              echo "No release notes provided."
+            fi
+          } > RELEASE_NOTES.md
📝 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
- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
run: |
{
echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
echo
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
} > RELEASE_NOTES.md
- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -eo pipefail
NOTES="$(printf '%s\n' "$PR_BODY" | awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
{
echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
echo
if [ -n "$NOTES" ]; then
printf '%s\n' "$NOTES"
else
echo "No release notes provided."
fi
} > RELEASE_NOTES.md
🤖 Prompt for AI Agents
In .github/workflows/react-native-cicd.yml around lines 296–304, replace the
current step that writes RELEASE_NOTES.md using the RELEASE_NOTES env var with
an inline generation that writes the file directly: create the header ("##
Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"), then append extracted
release notes obtained inline (prefer github.event.inputs.release_notes or
github.event.pull_request.body if present, otherwise fall back to recent commit
messages via git log), and write the combined content to RELEASE_NOTES.md in
this step (fail the job if extraction fails).

microphonePermission: 'Allow Resgrid Responder to access the microphone for audio input used in PTT and calls.',
},
],
'react-native-ble-manager',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect Expo config plugin ID for BLE Manager.

'react-native-ble-manager' is a library name, not the Expo config plugin. As-is, no native config changes will be applied (permissions, Gradle settings, etc.). Use the config plugin package.

-    'react-native-ble-manager',
+    '@config-plugins/react-native-ble-manager',

If you intentionally rely on manual permissions instead of a config plugin, please confirm and document it.

📝 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
'react-native-ble-manager',
'@config-plugins/react-native-ble-manager',
🤖 Prompt for AI Agents
In app.config.ts around line 262, the entry 'react-native-ble-manager' is the
npm library name, not an Expo config plugin, so no native changes will be
applied; fix by replacing that string with the library's actual Expo config
plugin package/name as documented by the BLE library (or the plugin path it
exports) so permissions/Gradle/manifest edits are applied automatically; if the
BLE library has no config plugin, remove this incorrect entry and add a clear
comment in the repo (and README) stating you are handling all Android/iOS
permissions and native setup manually, listing the exact manual steps and
required manifest/Gradle edits.

Comment on lines +218 to +229
// Close button
onPress={() => {
trackEvent('audio_stream_bottom_sheet_closed', {
timestamp: new Date().toISOString(),
hasCurrentStream: !!currentStream,
isPlaying,
timeSpent: Date.now(), // Could be improved with actual time tracking
});

setIsBottomSheetVisible(false);
}}
```
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix: timeSpent example is incorrect (uses absolute epoch time).

timeSpent should be the elapsed duration the bottom sheet was open, not Date.now(). Recommend documenting the correct pattern with an open timestamp and computing the delta on close.

Apply this diff to the example:

-  timeSpent: Date.now(), // Could be improved with actual time tracking
+  // Compute actual time spent (ms) since the bottom sheet became visible
+  timeSpent: Date.now() - openedAtTimestamp,

And add a short snippet demonstrating how to capture openedAtTimestamp:

// On open
const openedAtRef = React.useRef<number | null>(null);
useEffect(() => {
  if (isBottomSheetVisible) {
    openedAtRef.current = Date.now();
  } else {
    openedAtRef.current = null;
  }
}, [isBottomSheetVisible]);
🤖 Prompt for AI Agents
In docs/audio-stream-analytics-implementation.md around lines 218 to 229, the
example incorrectly sends timeSpent as an absolute epoch (Date.now()); change it
to compute elapsed time by storing an openedAt timestamp when the bottom sheet
opens (e.g., in a React ref) and computing timeSpent = Date.now() - openedAt
(with a safe fallback) when closing; update the onPress trackEvent to use that
delta and add a short snippet showing how to set openedAt on open and clear it
on close.

Comment on lines +20 to +23
- `hasPreferredDevice`: Boolean indicating if user has a preferred device set
- `preferredDeviceId`: ID of the preferred device (empty string if none)
- `connectedDeviceId`: ID of currently connected device (empty string if none)
- `bluetoothState`: Current Bluetooth adapter state
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace raw device IDs with anonymized hashes

These properties may capture persistent hardware identifiers. Use anonymized variants to avoid sending raw IDs.

-- `preferredDeviceId`: ID of the preferred device (empty string if none)
-- `connectedDeviceId`: ID of currently connected device (empty string if none)
+- `preferredDeviceHash`: Anonymized, salted hash of the preferred device identifier (empty string if none)
+- `connectedDeviceHash`: Anonymized, salted hash of the currently connected device identifier (empty string if none)
📝 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
- `hasPreferredDevice`: Boolean indicating if user has a preferred device set
- `preferredDeviceId`: ID of the preferred device (empty string if none)
- `connectedDeviceId`: ID of currently connected device (empty string if none)
- `bluetoothState`: Current Bluetooth adapter state
- `hasPreferredDevice`: Boolean indicating if user has a preferred device set
- `preferredDeviceHash`: Anonymized, salted hash of the preferred device identifier (empty string if none)
- `connectedDeviceHash`: Anonymized, salted hash of the currently connected device identifier (empty string if none)
- `bluetoothState`: Current Bluetooth adapter state
🧰 Tools
🪛 LanguageTool

[grammar] ~20-~20: There might be a mistake here.
Context: ...ating if user has a preferred device set - preferredDeviceId: ID of the preferred device (empty stri...

(QB_NEW_EN)


[grammar] ~21-~21: There might be a mistake here.
Context: ... preferred device (empty string if none) - connectedDeviceId: ID of currently connected device (empt...

(QB_NEW_EN)


[grammar] ~22-~22: There might be a mistake here.
Context: ... connected device (empty string if none) - bluetoothState: Current Bluetooth adapter state - `has...

(QB_NEW_EN)


[grammar] ~23-~23: There might be a mistake here.
Context: ...hState: Current Bluetooth adapter state - hasConnectionError`: Boolean indicating if there's a connec...

(QB_NEW_EN)

@@ -0,0 +1,251 @@
import React from 'react';
import { render } from '@testing-library/react-native';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Import waitFor to avoid race conditions with useEffect-driven tracking.

Analytics is typically emitted in a useEffect; asserting immediately can be flaky. Import waitFor here and use it in the relevant tests.

-import { render } from '@testing-library/react-native';
+import { render, waitFor } from '@testing-library/react-native';
📝 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
import { render } from '@testing-library/react-native';
import { render, waitFor } from '@testing-library/react-native';
🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet-analytics.test.tsx
around line 2, the test imports only render which can cause flaky assertions for
analytics emitted from useEffect; add an import for waitFor from
'@testing-library/react-native' and wrap the analytics assertions inside waitFor
(or replace immediate expects with await waitFor(() => { /* assertions */ })) so
the test waits for the useEffect-driven tracking to run before asserting.

Comment on lines +156 to +174
it('tracks analytics when sheet becomes visible', () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);

expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 1,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
});
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Await analytics emission and assert ISO timestamp format.

Guard against effect timing and validate timestamp quality.

-it('tracks analytics when sheet becomes visible', () => {
-  render(
-    <CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
-  );
-
-  expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
-    itemId: 'test-item-1',
-    itemType: 1,
-    hasLocation: true,
-    hasDescription: true,
-    isAllDay: false,
-    canSignUp: true,
-    isSignedUp: false,
-    attendeeCount: 1,
-    signupType: 1,
-    typeName: 'Meeting',
-    timestamp: expect.any(String),
-  });
-});
+it('tracks analytics when sheet becomes visible', async () => {
+  render(
+    <CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
+  );
+
+  await waitFor(() => {
+    expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', expect.objectContaining({
+      itemId: 'test-item-1',
+      itemType: 1,
+      hasLocation: true,
+      hasDescription: true,
+      isAllDay: false,
+      canSignUp: true,
+      isSignedUp: false,
+      attendeeCount: 1,
+      signupType: 1,
+      typeName: 'Meeting',
+      timestamp: expect.any(String),
+    }));
+  });
+
+  const [, payload] = mockTrackEvent.mock.calls[0];
+  expect(payload.timestamp).toMatch(ISO_8601_RE);
+});
📝 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
it('tracks analytics when sheet becomes visible', () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);
expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 1,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
});
});
it('tracks analytics when sheet becomes visible', async () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);
await waitFor(() => {
expect(mockTrackEvent).toHaveBeenCalledWith(
'calendar_item_details_viewed',
expect.objectContaining({
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 1,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
})
);
});
const [, payload] = mockTrackEvent.mock.calls[0];
expect(payload.timestamp).toMatch(ISO_8601_RE);
});
🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet-analytics.test.tsx
around lines 156 to 174, the test currently asserts mockTrackEvent immediately
and only checks timestamp is any String; update the test to wait for the
analytics effect to run (e.g., use async test with await waitFor or similar)
before asserting, and replace expect.any(String) with a strict ISO timestamp
check (e.g., match against /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/ or
use Date.parse to assert validity) so the test both waits for emission and
verifies ISO format for the timestamp.

@@ -0,0 +1,170 @@
import React from 'react';
import { render } from '@testing-library/react-native';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Import waitFor for effect timing.

Mirror the approach in the other test to avoid races.

-import { render } from '@testing-library/react-native';
+import { render, waitFor } from '@testing-library/react-native';
📝 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
import { render } from '@testing-library/react-native';
import { render, waitFor } from '@testing-library/react-native';
🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet-minimal.test.tsx
around line 2, the test is missing an import of waitFor which can cause
timing/race issues; add waitFor to the import from
'@testing-library/react-native' and update the test to use await waitFor(...)
around the assertions or any expectation that depends on effects/async updates
(mirroring the other test) so effects settle before asserting.

Comment on lines +117 to +135
it('tracks analytics when sheet becomes visible', () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);

expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 0,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
});
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Await analytics emission and assert ISO timestamp format.

Strengthen the test and reduce flakiness.

-it('tracks analytics when sheet becomes visible', () => {
-  render(
-    <CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
-  );
-
-  expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
-    itemId: 'test-item-1',
-    itemType: 1,
-    hasLocation: true,
-    hasDescription: true,
-    isAllDay: false,
-    canSignUp: true,
-    isSignedUp: false,
-    attendeeCount: 0,
-    signupType: 1,
-    typeName: 'Meeting',
-    timestamp: expect.any(String),
-  });
-});
+it('tracks analytics when sheet becomes visible', async () => {
+  render(
+    <CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
+  );
+  await waitFor(() =>
+    expect(mockTrackEvent).toHaveBeenCalledWith(
+      'calendar_item_details_viewed',
+      expect.objectContaining({
+        itemId: 'test-item-1',
+        itemType: 1,
+        hasLocation: true,
+        hasDescription: true,
+        isAllDay: false,
+        canSignUp: true,
+        isSignedUp: false,
+        attendeeCount: 0,
+        signupType: 1,
+        typeName: 'Meeting',
+        timestamp: expect.any(String),
+      })
+    )
+  );
+  const [, payload] = (mockTrackEvent as jest.Mock).mock.calls[0];
+  const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3,})?Z$/;
+  expect(payload.timestamp).toMatch(ISO_8601_RE);
+});
📝 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
it('tracks analytics when sheet becomes visible', () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);
expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_details_viewed', {
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 0,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
});
});
it('tracks analytics when sheet becomes visible', async () => {
render(
<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />
);
await waitFor(() =>
expect(mockTrackEvent).toHaveBeenCalledWith(
'calendar_item_details_viewed',
expect.objectContaining({
itemId: 'test-item-1',
itemType: 1,
hasLocation: true,
hasDescription: true,
isAllDay: false,
canSignUp: true,
isSignedUp: false,
attendeeCount: 0,
signupType: 1,
typeName: 'Meeting',
timestamp: expect.any(String),
})
)
);
const [, payload] = (mockTrackEvent as jest.Mock).mock.calls[0];
const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3,})?Z$/;
expect(payload.timestamp).toMatch(ISO_8601_RE);
});
🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet-minimal.test.tsx
around lines 117 to 135, the test currently asserts analytics immediately and
uses expect.any(String) for timestamp which is flaky; change the test to be
async, wait for the analytics call to occur (e.g. use waitFor or similar) and
replace the loose timestamp assertion with an ISO-8601 format check (use
expect.stringMatching with a regex matching full ISO timestamps, e.g.
YYYY-MM-DDTHH:MM:SS(.sss)?Z) so the test awaits emission and validates the
timestamp format reliably.

Comment on lines +53 to +63
#### 4. Calendar Item Attendance Failed
**Event Name:** `calendar_item_attendance_failed`

**Triggered When:** Attendance status change fails

**Properties:**
- `itemId` (string): Unique identifier for the calendar item
- `attending` (boolean): Attempted attendance status
- `error` (string): Error message
- `timestamp` (string): ISO timestamp

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid sending raw error messages in analytics payloads (potential PII leak).

The calendar_item_attendance_failed event currently documents error as a free-form string. Raw error messages can include PII or sensitive server details. Prefer a stable error code/category and optionally a sanitized summary.

Proposed doc update:

- - `error` (string): Error message
+ - `errorCode` (string): Stable, non-PII identifier for the error (e.g., "NETWORK_TIMEOUT", "UNAUTHORIZED")
+ - `errorCategory` (string): High-level category (e.g., "network", "auth", "validation")
+ - `errorMessage` (string, optional): Sanitized, non-PII message suitable for analytics

Please also verify the implementation emits only sanitized values for failures and does not include exception stacks or user input. If you want, I can draft a small error-mapping utility and update the component.


🏁 Script executed:

#!/bin/bash
# Search for analytics event emission in the codebase
rg -nP "calendar_item_attendance_failed" -C3

Length of output: 2158


Enforce non-PII error reporting for attendance-failure analytics
The implementation currently sends raw error.message (see src/components/calendar/calendar-item-details-sheet.tsx:138), and tests expect that exact string. To prevent potential PII leaks, we should refactor both the code and docs to use structured error metadata:

• Affected files:
– src/components/calendar/calendar-item-details-sheet.tsx (update trackEvent payload)
– src/components/calendar/tests/calendar-item-details-sheet.test.tsx (update expectations)
– src/components/calendar/README-analytics.md (update schema)

• Doc diff proposal:

--- a/src/components/calendar/README-analytics.md
+++ b/src/components/calendar/README-analytics.md
@@ -60,7 +60,10 @@
- `error` (string): Error message
+ `errorCode` (string): Stable, non-PII identifier (e.g., "NETWORK_TIMEOUT", "UNAUTHORIZED")
+ `errorCategory` (string): High-level category (e.g., "network", "auth", "validation")
+ `errorMessage` (string, optional): Sanitized, non-PII summary suitable for analytics

• Code sketch:

const { code, category, message } = mapError(error);
trackEvent('calendar_item_attendance_failed', {
  itemId: item.CalendarItemId,
  attending,
  errorCode: code,
  errorCategory: category,
  errorMessage: message,
});

Please implement an error-mapping utility, update the component to emit only these fields, and adjust the corresponding tests.

🧰 Tools
🪛 LanguageTool

[grammar] ~53-~53: There might be a mistake here.
Context: ... #### 4. Calendar Item Attendance Failed Event Name: `calendar_item_attendance_...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In src/components/calendar/README-analytics.md around lines 53-63 and in the
implementation at src/components/calendar/calendar-item-details-sheet.tsx
(referenced ~line 138) plus tests at
src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx, the code
currently emits raw error.message for attendance failures; replace this with
structured, non-PII fields. Create a small error-mapping utility (e.g.,
mapError(error) -> { code, category, message }) that extracts a safe errorCode,
errorCategory and a sanitized errorMessage (no PII), update the component to
call mapError(error) and send trackEvent('calendar_item_attendance_failed', {
itemId, attending, errorCode, errorCategory, errorMessage, timestamp }), update
the unit test expectations to assert these three fields instead of raw
error.message, and update the README-analytics.md schema to list errorCode
(string), errorCategory (string) and errorMessage (string) instead of a raw
error property.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +122 to +126
### Data Privacy
- No sensitive call content is tracked
- Only metadata and interaction patterns captured
- Follows existing analytics privacy patterns

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Privacy claim contradicts tracked payloads (addresses and precise coordinates).

The "Data Privacy" section states no sensitive content is tracked, but the implementation (see src/app/call/new/index.tsx) logs precise latitude/longitude and the full formatted address when selecting a result. That’s potentially PII.

Either:

  • Update the docs to explicitly state that precise location/address data is tracked and justify it, or
  • Change the implementation to de-identify/anonymize:
    • Round coordinates to a coarse precision (e.g., 2–3 decimals) or bucket them.
    • Remove selectedAddress from analytics, or hash/truncate to a non-identifying level (e.g., city/locality only).

I can draft the code changes if you choose the anonymization route.

🧰 Tools
🪛 LanguageTool

[grammar] ~122-~122: There might be a mistake here.
Context: ...en call data is loaded ### Data Privacy - No sensitive call content is tracked - O...

(QB_NEW_EN)

Comment on lines +24 to +26
- **Download Failure**: `call_file_download_failed`
- Properties: `timestamp`, `callId`, `fileId`, `fileName`, `error`

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Sanitize error details for analytics safety.

Avoid emitting raw error strings; prefer structured codes/names and optionally a truncated summary to prevent PII or secret leakage.

Apply this doc tweak:

-    - Properties: `timestamp`, `callId`, `fileId`, `fileName`, `error`
+    - Properties: `timestamp`, `callId`, `fileId`, `fileName`, `errorCode`, `errorName`, `errorMessage` (truncated, sanitized)

Also consider the same for call_files_retry_pressed properties below if it currently includes a raw error.

📝 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
- **Download Failure**: `call_file_download_failed`
- Properties: `timestamp`, `callId`, `fileId`, `fileName`, `error`
- **Download Failure**: `call_file_download_failed`
- Properties: `timestamp`, `callId`, `fileId`, `fileName`, `errorCode`, `errorName`, `errorMessage` (truncated, sanitized)
🧰 Tools
🪛 LanguageTool

[grammar] ~24-~24: There might be a mistake here.
Context: ...aredboolean - **Download Failure**:call_file_download_failed - Properties:timestamp, callId, file...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/call-files-modal-analytics-implementation.md around lines 24-26, the
spec currently emits a raw `error` string for `call_file_download_failed`;
update the event properties to use a structured error field (e.g., `errorCode`
or `errorName`) and an optional sanitized/truncated `errorSummary` (max length,
remove PII/secrets via whitelist or regex) instead of the raw message; ensure
the same change is applied to `call_files_retry_pressed` if it also includes a
raw `error`, and add a note describing the sanitization/truncation rules and max
length.

Comment on lines +52 to +62
#### Close Failure Analytics
- **Event Name**: `close_call_failed`
- **Triggered**: When the API call to close the call fails
- **Data Tracked**:
- `timestamp`: ISO string timestamp
- `callId`: ID of the call
- `closeType`: Close type attempted
- `hasNote`: Whether a note was provided
- `noteLength`: Length of the note text
- `error`: Error message from the failed API call

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid sending raw error messages in analytics payloads.

Raw error strings may leak PII, tokens, or backend internals. Track sanitized error metadata instead (code/name) and optionally a truncated message.

Apply this doc tweak:

-  - `error`: Error message from the failed API call
+  - `errorCode`: Stable error code or HTTP status (e.g., 400, 401, 500)
+  - `errorName`: Short error classification (e.g., "validation_error", "network_error")
+  - `errorMessage`: Truncated and sanitized summary (optional, max 200 chars)

If useful, I can produce a cross-repo grep to surface other spots sending raw error strings and propose patches.

📝 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
#### Close Failure Analytics
- **Event Name**: `close_call_failed`
- **Triggered**: When the API call to close the call fails
- **Data Tracked**:
- `timestamp`: ISO string timestamp
- `callId`: ID of the call
- `closeType`: Close type attempted
- `hasNote`: Whether a note was provided
- `noteLength`: Length of the note text
- `error`: Error message from the failed API call
#### Close Failure Analytics
- **Event Name**: `close_call_failed`
- **Triggered**: When the API call to close the call fails
- **Data Tracked**:
- `timestamp`: ISO string timestamp
- `callId`: ID of the call
- `closeType`: Close type attempted
- `hasNote`: Whether a note was provided
- `noteLength`: Length of the note text
- `errorCode`: Stable error code or HTTP status (e.g., 400, 401, 500)
- `errorName`: Short error classification (e.g., "validation_error", "network_error")
- `errorMessage`: Truncated and sanitized summary (optional, max 200 chars)
🧰 Tools
🪛 LanguageTool

[grammar] ~52-~52: There might be a mistake here.
Context: ... note text #### Close Failure Analytics - Event Name: close_call_failed - **Tr...

(QB_NEW_EN)


[grammar] ~53-~53: There might be a mistake here.
Context: ...ose Failure Analytics - Event Name: close_call_failed - Triggered: When the API call to close ...

(QB_NEW_EN)


[grammar] ~54-~54: There might be a mistake here.
Context: ...hen the API call to close the call fails - Data Tracked: - timestamp: ISO str...

(QB_NEW_EN)


[grammar] ~55-~55: There might be a mistake here.
Context: ...close the call fails - Data Tracked: - timestamp: ISO string timestamp - callId: ID ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...*: - timestamp: ISO string timestamp - callId: ID of the call - closeType: Close ...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ...g timestamp - callId: ID of the call - closeType: Close type attempted - hasNote: Wh...

(QB_NEW_EN)


[grammar] ~58-~58: There might be a mistake here.
Context: ...ll - closeType: Close type attempted - hasNote: Whether a note was provided - `noteL...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ...- hasNote: Whether a note was provided - noteLength: Length of the note text - error: E...

(QB_NEW_EN)


[grammar] ~60-~60: There might be a mistake here.
Context: ... - noteLength: Length of the note text - error: Error message from the failed API call...

(QB_NEW_EN)

Comment on lines +63 to +72
#### Manual Close Analytics
- **Event Name**: `close_call_bottom_sheet_closed`
- **Triggered**: When user manually closes the bottom sheet (cancel button)
- **Data Tracked**:
- `timestamp`: ISO string timestamp
- `callId`: ID of the call
- `wasManualClose`: Always true for manual closes
- `hadCloseCallType`: Whether a close type was selected
- `hadCloseCallNote`: Whether a note was entered

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Generalize the close event to cover gesture/system closes (with wasManualClose flag).

Currently documented as “manual close.” Align with other modal docs by treating this as a general “closed” event with a boolean flag.

Apply this doc tweak:

-#### Manual Close Analytics
-- **Event Name**: `close_call_bottom_sheet_closed`
-- **Triggered**: When user manually closes the bottom sheet (cancel button)
-  - `wasManualClose`: Always true for manual closes
+#### Close Analytics
+- **Event Name**: `close_call_bottom_sheet_closed`
+- **Triggered**: When the bottom sheet closes (cancel button, gesture, backdrop, back button)
+  - `wasManualClose`: true for explicit cancel button, false for gesture/back/backdrop
🧰 Tools
🪛 LanguageTool

[grammar] ~63-~63: There might be a mistake here.
Context: ...ed API call #### Manual Close Analytics - Event Name: `close_call_bottom_sheet_c...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...anual Close Analytics - Event Name: close_call_bottom_sheet_closed - Triggered: When user manually closes t...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ... closes the bottom sheet (cancel button) - Data Tracked: - timestamp: ISO str...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...heet (cancel button) - Data Tracked: - timestamp: ISO string timestamp - callId: ID ...

(QB_NEW_EN)


[grammar] ~67-~67: There might be a mistake here.
Context: ...*: - timestamp: ISO string timestamp - callId: ID of the call - wasManualClose: A...

(QB_NEW_EN)


[grammar] ~68-~68: There might be a mistake here.
Context: ...g timestamp - callId: ID of the call - wasManualClose: Always true for manual closes - `had...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...ualClose: Always true for manual closes - hadCloseCallType`: Whether a close type was selected - ...

(QB_NEW_EN)


[grammar] ~70-~70: There might be a mistake here.
Context: ...Type: Whether a close type was selected - hadCloseCallNote`: Whether a note was entered ### 3. Err...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/close-call-bottom-sheet-analytics-implementation.md around lines 63 to
72, change the section from “Manual Close Analytics” to a generalized “Close
Analytics”, update the Triggered line to “When the bottom sheet is closed
(manual, gesture, or system)”, and document that the event tracks a boolean
wasManualClose (true only when closed via the cancel button, false for
gesture/system closes); keep the other fields (timestamp, callId,
hadCloseCallType, hadCloseCallNote) unchanged and ensure wording aligns with
other modal docs.

Comment on lines +19 to +21
- `timestamp`: ISO string of when the event occurred
- `username`: The username attempted (for analytics purposes)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not send raw usernames to analytics; use a one-way hash or stable pseudonymous ID instead.

Capturing username is PII and increases risk (GDPR/CCPA/HIPAA implications). Replace the raw value with a hashed or mapped identifier.

Apply these diffs to the documentation:

-  - `username`: The username attempted (for analytics purposes)
+  - `user_id_hash`: One-way hash of the username (no raw PII)
   trackEvent('login_attempted', {
     timestamp: new Date().toISOString(),
-    username: data.username,
+    user_id_hash: await hashUsername(data.username),
   });

Outside this block, add a helper example to make the approach concrete:

// Example helper (React Native/Expo)
import * as Crypto from 'expo-crypto';

async function hashUsername(username: string) {
  return await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    username.trim().toLowerCase()
  );
}

Also applies to: 57-61

🤖 Prompt for AI Agents
In docs/login-analytics-implementation.md around lines 19-21 (and similarly
lines 57-61), the doc currently records raw `username` which is PII; replace
`username` with a one-way hashed or stable pseudonymous identifier (e.g.,
`user_id_hash`) and describe that the hash must be computed client-side or
before logging using a strong algorithm (SHA-256), normalizing input (trim and
lowercase) and without reversible salts; update example payload fields
accordingly and, outside the existing block, add a short helper example (React
Native/Expo) that demonstrates computing the SHA-256 hash of the normalized
username using expo-crypto as the concrete implementation reference.

Comment on lines +72 to +92
// Simulate the onSubmit function pattern
const onSubmit = jest.fn(async (data) => {
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
username: data.username,
});
await mockLogin({ username: data.username, password: data.password });
});

const testData = { username: 'testuser', password: 'testpass' };
onSubmit(testData);

expect(mockTrackEvent).toHaveBeenCalledWith('login_attempted', {
timestamp: expect.any(String),
username: 'testuser',
});
expect(mockLogin).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass',
});
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update onSubmit analytics to avoid PII (use hasUsername).

Also adjust the assertion accordingly.

     const onSubmit = jest.fn(async (data) => {
-      trackEvent('login_attempted', {
-        timestamp: new Date().toISOString(),
-        username: data.username,
-      });
+      trackEvent('login_attempted', {
+        timestamp: new Date().toISOString(),
+        hasUsername: Boolean(data.username),
+      });
       await mockLogin({ username: data.username, password: data.password });
     });
@@
-    expect(mockTrackEvent).toHaveBeenCalledWith('login_attempted', {
-      timestamp: expect.any(String),
-      username: 'testuser',
-    });
+    expect(mockTrackEvent).toHaveBeenCalledWith('login_attempted', {
+      timestamp: expect.any(String),
+      hasUsername: true,
+    });
📝 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
// Simulate the onSubmit function pattern
const onSubmit = jest.fn(async (data) => {
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
username: data.username,
});
await mockLogin({ username: data.username, password: data.password });
});
const testData = { username: 'testuser', password: 'testpass' };
onSubmit(testData);
expect(mockTrackEvent).toHaveBeenCalledWith('login_attempted', {
timestamp: expect.any(String),
username: 'testuser',
});
expect(mockLogin).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass',
});
});
// Simulate the onSubmit function pattern
const onSubmit = jest.fn(async (data) => {
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
hasUsername: Boolean(data.username),
});
await mockLogin({ username: data.username, password: data.password });
});
const testData = { username: 'testuser', password: 'testpass' };
onSubmit(testData);
expect(mockTrackEvent).toHaveBeenCalledWith('login_attempted', {
timestamp: expect.any(String),
hasUsername: true,
});
expect(mockLogin).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass',
});
});
🤖 Prompt for AI Agents
In src/app/login/__tests__/index.test.tsx around lines 72 to 92, the test
currently asserts that the analytics event includes the raw username (PII);
change the tracked payload to use hasUsername boolean instead of username, and
update the assertion accordingly. Specifically, modify the onSubmit simulation
to call trackEvent('login_attempted', { timestamp: ..., hasUsername:
Boolean(data.username) }) and then update the
expect(mockTrackEvent).toHaveBeenCalledWith(...) to expect hasUsername: true
(keep timestamp as expect.any(String)), while leaving the mockLogin assertion
unchanged.

Comment on lines +169 to +202
it('should validate analytics data structure', () => {
// Test that all required analytics properties are correctly formatted
const loginViewedData = {
timestamp: new Date().toISOString(),
};

const loginAttemptedData = {
timestamp: new Date().toISOString(),
username: 'testuser',
};

const loginSuccessData = {
timestamp: new Date().toISOString(),
};

const loginFailedData = {
timestamp: new Date().toISOString(),
error: 'Invalid credentials',
};

// Validate data types
expect(typeof loginViewedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.username).toBe('string');
expect(typeof loginSuccessData.timestamp).toBe('string');
expect(typeof loginFailedData.timestamp).toBe('string');
expect(typeof loginFailedData.error).toBe('string');

// Validate timestamp format
expect(Date.parse(loginViewedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginAttemptedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginSuccessData.timestamp)).not.toBeNaN();
expect(Date.parse(loginFailedData.timestamp)).not.toBeNaN();
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adjust data structure validation to reflect non-PII analytics.

Validate hasUsername is a boolean and timestamps remain parseable.

     const loginAttemptedData = {
       timestamp: new Date().toISOString(),
-      username: 'testuser',
+      hasUsername: true,
     };
@@
-    expect(typeof loginAttemptedData.username).toBe('string');
+    expect(typeof loginAttemptedData.hasUsername).toBe('boolean');
@@
     expect(Date.parse(loginAttemptedData.timestamp)).not.toBeNaN();
📝 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
it('should validate analytics data structure', () => {
// Test that all required analytics properties are correctly formatted
const loginViewedData = {
timestamp: new Date().toISOString(),
};
const loginAttemptedData = {
timestamp: new Date().toISOString(),
username: 'testuser',
};
const loginSuccessData = {
timestamp: new Date().toISOString(),
};
const loginFailedData = {
timestamp: new Date().toISOString(),
error: 'Invalid credentials',
};
// Validate data types
expect(typeof loginViewedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.username).toBe('string');
expect(typeof loginSuccessData.timestamp).toBe('string');
expect(typeof loginFailedData.timestamp).toBe('string');
expect(typeof loginFailedData.error).toBe('string');
// Validate timestamp format
expect(Date.parse(loginViewedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginAttemptedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginSuccessData.timestamp)).not.toBeNaN();
expect(Date.parse(loginFailedData.timestamp)).not.toBeNaN();
});
it('should validate analytics data structure', () => {
// Test that all required analytics properties are correctly formatted
const loginViewedData = {
timestamp: new Date().toISOString(),
};
const loginAttemptedData = {
timestamp: new Date().toISOString(),
hasUsername: true,
};
const loginSuccessData = {
timestamp: new Date().toISOString(),
};
const loginFailedData = {
timestamp: new Date().toISOString(),
error: 'Invalid credentials',
};
// Validate data types
expect(typeof loginViewedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.timestamp).toBe('string');
expect(typeof loginAttemptedData.hasUsername).toBe('boolean');
expect(typeof loginSuccessData.timestamp).toBe('string');
expect(typeof loginFailedData.timestamp).toBe('string');
expect(typeof loginFailedData.error).toBe('string');
// Validate timestamp format
expect(Date.parse(loginViewedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginAttemptedData.timestamp)).not.toBeNaN();
expect(Date.parse(loginSuccessData.timestamp)).not.toBeNaN();
expect(Date.parse(loginFailedData.timestamp)).not.toBeNaN();
});
🤖 Prompt for AI Agents
In src/app/login/__tests__/index.test.tsx around lines 169 to 202, the analytics
test should use a non-PII flag for username presence rather than sending the
actual username; add a hasUsername boolean property to the relevant analytics
objects (e.g., loginAttemptedData and any other events that previously included
username), assert that hasUsername is a boolean, and keep the existing
assertions that all timestamps are strings and parseable; update/remove any
assertions that expect the raw username string so the test validates non-PII
analytics only.

Comment on lines +55 to +60
// Track login failure
trackEvent('login_failed', {
timestamp: new Date().toISOString(),
error: error || 'Unknown error',
});

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sanitize error analytics and guard with try/catch.

Raw error strings can include sensitive info. Prefer a bounded, classified error signal.

-      // Track login failure
-      trackEvent('login_failed', {
-        timestamp: new Date().toISOString(),
-        error: error || 'Unknown error',
-      });
+      // Track login failure (sanitized)
+      try {
+        const safeError =
+          typeof error === 'string'
+            ? error
+            : (error && (error.code || error.name)) || 'Unknown error';
+        // Optionally: limit length
+        const boundedError =
+          typeof safeError === 'string' ? safeError.slice(0, 128) : 'Unknown error';
+        trackEvent('login_failed', {
+          timestamp: new Date().toISOString(),
+          error: boundedError,
+        });
+      } catch (e) {
+        console.warn('Analytics failed (login_failed):', e);
+      }
📝 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
// Track login failure
trackEvent('login_failed', {
timestamp: new Date().toISOString(),
error: error || 'Unknown error',
});
// Track login failure (sanitized)
try {
const safeError =
typeof error === 'string'
? error
: (error && (error.code || error.name)) || 'Unknown error';
// Optionally: limit length
const boundedError =
typeof safeError === 'string' ? safeError.slice(0, 128) : 'Unknown error';
trackEvent('login_failed', {
timestamp: new Date().toISOString(),
error: boundedError,
});
} catch (e) {
console.warn('Analytics failed (login_failed):', e);
}
🤖 Prompt for AI Agents
In src/app/login/index.tsx around lines 55-60 the code sends the raw error to
analytics; change this to classify and truncate the error and guard the
analytics call with try/catch: derive a safeErrorCategory or errorCode from
known/whitelisted error types (fall back to "unknown_error"), build a minimal
payload with only bounded fields (e.g. { timestamp, errorCode, category,
message: truncatedMessage <=100 chars }) and do NOT include stack traces or full
error strings, then call trackEvent with that payload inside a try/catch so any
analytics failure is swallowed (optionally log a non-sensitive local warning).

Comment on lines 66 to 69
logger.info({
message: 'Starting Login (button press)',
context: { username: data.username },
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid logging raw usernames in application logs.

This is PII in logs. Prefer a boolean signal or a safe hash if necessary.

-  logger.info({
-    message: 'Starting Login (button press)',
-    context: { username: data.username },
-  });
+  logger.info({
+    message: 'Starting Login (button press)',
+    // Avoid logging PII
+    context: { hasUsername: Boolean(data.username) },
+  });
📝 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
logger.info({
message: 'Starting Login (button press)',
context: { username: data.username },
});
logger.info({
message: 'Starting Login (button press)',
// Avoid logging PII
context: { hasUsername: Boolean(data.username) },
});
🤖 Prompt for AI Agents
In src/app/login/index.tsx around lines 66 to 69, the code currently logs the
raw username which is PII; remove the plain username from logs and replace it
with either a boolean flag (e.g., { hasUsername: !!data.username }) or an
anonymized identifier such as a one-way salted hash (compute a secure hash/HMAC
using an application salt/key from config and log that value) so logs never
contain the raw username; ensure the salt/key is retrieved from secure config
and that the hashing is non-reversible before updating the logger call.

Comment on lines +71 to +76
// Track login attempt
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
username: data.username,
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not send raw username to analytics; replace with a non-PII signal and wrap in try/catch.

Sending usernames to analytics is a privacy risk (PII leakage). Track only presence or a non-reversible hash, and guard analytics calls.

-    // Track login attempt
-    trackEvent('login_attempted', {
-      timestamp: new Date().toISOString(),
-      username: data.username,
-    });
+    // Track login attempt (no PII)
+    try {
+      trackEvent('login_attempted', {
+        timestamp: new Date().toISOString(),
+        hasUsername: Boolean(data.username),
+      });
+    } catch (e) {
+      console.warn('Analytics failed (login_attempted):', e);
+    }
📝 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
// Track login attempt
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
username: data.username,
});
// Track login attempt (no PII)
try {
trackEvent('login_attempted', {
timestamp: new Date().toISOString(),
hasUsername: Boolean(data.username),
});
} catch (e) {
console.warn('Analytics failed (login_attempted):', e);
}
🤖 Prompt for AI Agents
In src/app/login/index.tsx around lines 71 to 76, the code sends raw usernames
to analytics; replace that with a non-PII signal (e.g., a boolean "hasUsername"
or a non-reversible hash of the username like SHA-256) and wrap the analytics
call in a try/catch to prevent runtime errors from bubbling up. Compute the hash
or boolean locally, do not include the raw username in the payload, and call
trackEvent inside a try block with errors caught and logged (or ignored) in the
catch.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +559 to +592
it('tracks recenter map action', async () => {
const mockLocationStore = jest.requireMock('@/stores/app/location-store') as any;
mockLocationStore.useLocationStore.mockReturnValue({
latitude: 40.7128,
longitude: -74.006,
heading: 90,
isMapLocked: false,
});

render(<HomeMap />);

// Wait for map to be ready and simulate user moving map
await waitFor(() => {
expect(screen.getByTestId('home-map-view')).toBeTruthy();
});

// Clear initial analytics call
mockTrackEvent.mockClear();

// Note: In the actual implementation, the recenter button would only show
// when hasUserMovedMap is true, but for testing we'll just verify the handler
const recenterButton = screen.queryByTestId('recenter-button');
if (recenterButton) {
fireEvent.press(recenterButton);

// Check analytics tracking for recenter action
expect(mockTrackEvent).toHaveBeenCalledWith('map_recentered', {
timestamp: expect.any(String),
isMapLocked: false,
zoomLevel: 12,
});
}
});

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Recenter analytics test is currently inert; assert and trigger deterministically

Because the test presses the recenter button only if it exists, it can silently pass without asserting anything. With the MapView mock triggering user interaction, assert the button exists and the analytics event is emitted.

-    // Note: In the actual implementation, the recenter button would only show 
-    // when hasUserMovedMap is true, but for testing we'll just verify the handler
-    const recenterButton = screen.queryByTestId('recenter-button');
-    if (recenterButton) {
-      fireEvent.press(recenterButton);
-
-      // Check analytics tracking for recenter action
-      expect(mockTrackEvent).toHaveBeenCalledWith('map_recentered', {
-        timestamp: expect.any(String),
-        isMapLocked: false,
-        zoomLevel: 12,
-      });
-    }
+    // With MapView mock simulating user movement, the recenter button should exist
+    const recenterButton = await screen.findByTestId('recenter-button');
+    fireEvent.press(recenterButton);
+
+    // Check analytics tracking for recenter action
+    expect(mockTrackEvent).toHaveBeenCalledWith('map_recentered', {
+      timestamp: expect.any(String),
+      isMapLocked: false,
+      zoomLevel: 12,
+    });
📝 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
it('tracks recenter map action', async () => {
const mockLocationStore = jest.requireMock('@/stores/app/location-store') as any;
mockLocationStore.useLocationStore.mockReturnValue({
latitude: 40.7128,
longitude: -74.006,
heading: 90,
isMapLocked: false,
});
render(<HomeMap />);
// Wait for map to be ready and simulate user moving map
await waitFor(() => {
expect(screen.getByTestId('home-map-view')).toBeTruthy();
});
// Clear initial analytics call
mockTrackEvent.mockClear();
// Note: In the actual implementation, the recenter button would only show
// when hasUserMovedMap is true, but for testing we'll just verify the handler
const recenterButton = screen.queryByTestId('recenter-button');
if (recenterButton) {
fireEvent.press(recenterButton);
// Check analytics tracking for recenter action
expect(mockTrackEvent).toHaveBeenCalledWith('map_recentered', {
timestamp: expect.any(String),
isMapLocked: false,
zoomLevel: 12,
});
}
});
it('tracks recenter map action', async () => {
const mockLocationStore = jest.requireMock('@/stores/app/location-store') as any;
mockLocationStore.useLocationStore.mockReturnValue({
latitude: 40.7128,
longitude: -74.006,
heading: 90,
isMapLocked: false,
});
render(<HomeMap />);
// Wait for map to be ready and simulate user moving map
await waitFor(() => {
expect(screen.getByTestId('home-map-view')).toBeTruthy();
});
// Clear initial analytics call
mockTrackEvent.mockClear();
// With MapView mock simulating user movement, the recenter button should exist
const recenterButton = await screen.findByTestId('recenter-button');
fireEvent.press(recenterButton);
// Check analytics tracking for recenter action
expect(mockTrackEvent).toHaveBeenCalledWith('map_recentered', {
timestamp: expect.any(String),
isMapLocked: false,
zoomLevel: 12,
});
});
🤖 Prompt for AI Agents
In src/app/(app)/__tests__/map.test.tsx around lines 559 to 592, the test
conditionally presses the recenter button which can make the test pass silently
if the button is not rendered; update the test to deterministically assert the
recenter button exists (e.g. expect(recenterButton).toBeTruthy()), ensure the
component is rendered in a state that shows the recenter button (set the
mock/location or MapView mock so hasUserMovedMap is true or simulate user
movement), then fireEvent.press(recenterButton) and assert mockTrackEvent was
called with the expected payload (including timestamp as any(String) and other
properties).

Comment on lines +595 to +613
it('hides compose FAB when in selection mode even if user can create messages', () => {
mockedUseMessagesStore.mockReturnValue({
...mockStore,
selectedForDeletion: new Set(['1']),
hasSelectedMessages: jest.fn(() => true),
});
mockedUseSecurityStore.mockReturnValue({
...mockSecurityStore,
canUserCreateMessages: true,
});

// Simulate entering selection mode by triggering long press
render(<MessagesScreen />);

// Since we can't easily test the selection mode state change,
// we'll test that the FAB is hidden when selectedForDeletion has items
// This is an indirect test, but validates the behavior
expect(screen.getByTestId('messages-compose-fab')).toBeTruthy();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix selection-mode FAB visibility test to actually enter selection mode and assert hidden

Current test expects the FAB to be present even though the description says it should be hidden. Also, toggling selectedForDeletion alone doesn’t flip the internal isSelectionMode state. Simulate a long-press to enter selection mode, then assert the FAB is not rendered.

-    it('hides compose FAB when in selection mode even if user can create messages', () => {
-      mockedUseMessagesStore.mockReturnValue({
-        ...mockStore,
-        selectedForDeletion: new Set(['1']),
-        hasSelectedMessages: jest.fn(() => true),
-      });
-      mockedUseSecurityStore.mockReturnValue({
-        ...mockSecurityStore,
-        canUserCreateMessages: true,
-      });
-
-      // Simulate entering selection mode by triggering long press
-      render(<MessagesScreen />);
-
-      // Since we can't easily test the selection mode state change,
-      // we'll test that the FAB is hidden when selectedForDeletion has items
-      // This is an indirect test, but validates the behavior
-      expect(screen.getByTestId('messages-compose-fab')).toBeTruthy();
-    });
+    it('hides compose FAB when in selection mode even if user can create messages', () => {
+      mockedUseSecurityStore.mockReturnValue({
+        ...mockSecurityStore,
+        canUserCreateMessages: true,
+      });
+      render(<MessagesScreen />);
+      // Enter selection mode via long press on a message card
+      const messageCard = screen.getByTestId('message-card-1');
+      fireEvent(messageCard, 'longPress');
+      // FAB should now be hidden
+      expect(screen.queryByTestId('messages-compose-fab')).toBeNull();
+    });

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +32 to +35
describe('Notes Screen Analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

These tests don’t exercise the Notes screen; they only call the mock

This provides little value and wouldn’t catch regressions (e.g., event name mismatches). Render Notes, simulate focus/search/select, and assert analytics calls.

I can provide a test scaffold using @testing-library/react-native to render Notes, mock the store, simulate input, and verify trackEvent invocations.

Do you want me to generate a concrete test that renders the Notes screen and covers viewed/searched/refreshed/note_selected flows?

🤖 Prompt for AI Agents
In src/app/(app)/__tests__/notes.test.tsx around lines 32 to 35, the current
tests only invoke the analytics mock and do not render the Notes screen or
simulate user flows; update the test to render the actual Notes component with
@testing-library/react-native, provide necessary mocked store/state/providers,
then simulate screen focus (or navigation), perform a search input change,
trigger a refresh action, and select a note; finally assert that the analytics
tracking function (trackEvent) was called with the expected event names and
payloads for viewed, searched, refreshed, and note_selected events. Ensure you
mock any network/data hooks and navigation to avoid flakiness and call
jest.clearAllMocks in beforeEach as already present.

Comment on lines +37 to +56
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');

it('returns all notes when search query is empty', () => {
expect(filterNotes(testNotes, '')).toHaveLength(4);
expect(filterNotes(testNotes, ' ')).toHaveLength(4); // Whitespace only
});
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();

it('filters notes by title correctly', () => {
const result = filterNotes(testNotes, 'meeting');
expect(result).toHaveLength(1);
expect(result[0].NoteId).toBe('1');
// Simulate the analytics tracking that happens in useFocusEffect
trackEvent('notes_view', {
noteCount: notes.length,
hasSearchQuery: false,
currentCategory: 'All',
});

it('filters notes by body content correctly', () => {
const result = filterNotes(testNotes, 'birthday');
expect(result).toHaveLength(1);
expect(result[0].NoteId).toBe('2');
expect(mockTrackEvent).toHaveBeenCalledWith('notes_view', {
noteCount: 0,
hasSearchQuery: false,
currentCategory: 'All',
});
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Event name mismatch with implementation (“notes_view” vs “notes_viewed”)

The Notes screen emits notes_viewed; this test asserts notes_view. Align the test to the codebase convention.

Apply this diff:

-    trackEvent('notes_view', {
+    trackEvent('notes_viewed', {
       noteCount: notes.length,
       hasSearchQuery: false,
       currentCategory: 'All',
     });
 
-    expect(mockTrackEvent).toHaveBeenCalledWith('notes_view', {
+    expect(mockTrackEvent).toHaveBeenCalledWith('notes_viewed', {
       noteCount: 0,
       hasSearchQuery: false,
       currentCategory: 'All',
     });
📝 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
it('tracks notes view analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');
it('returns all notes when search query is empty', () => {
expect(filterNotes(testNotes, '')).toHaveLength(4);
expect(filterNotes(testNotes, ' ')).toHaveLength(4); // Whitespace only
});
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();
it('filters notes by title correctly', () => {
const result = filterNotes(testNotes, 'meeting');
expect(result).toHaveLength(1);
expect(result[0].NoteId).toBe('1');
// Simulate the analytics tracking that happens in useFocusEffect
trackEvent('notes_view', {
noteCount: notes.length,
hasSearchQuery: false,
currentCategory: 'All',
});
it('filters notes by body content correctly', () => {
const result = filterNotes(testNotes, 'birthday');
expect(result).toHaveLength(1);
expect(result[0].NoteId).toBe('2');
expect(mockTrackEvent).toHaveBeenCalledWith('notes_view', {
noteCount: 0,
hasSearchQuery: false,
currentCategory: 'All',
});
});
it('tracks notes view analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();
// Simulate the analytics tracking that happens in useFocusEffect
trackEvent('notes_viewed', {
noteCount: notes.length,
hasSearchQuery: false,
currentCategory: 'All',
});
expect(mockTrackEvent).toHaveBeenCalledWith('notes_viewed', {
noteCount: 0,
hasSearchQuery: false,
currentCategory: 'All',
});
});
🤖 Prompt for AI Agents
In src/app/(app)/__tests__/notes.test.tsx around lines 37 to 56, the test
currently uses the event name "notes_view" but the app emits "notes_viewed";
update the test to call and expect "notes_viewed" instead of "notes_view" so the
asserted event name matches the implementation (change both the trackEvent
invocation and the expect to use "notes_viewed").

Comment on lines +58 to +75
const { useAnalytics } = require('@/hooks/use-analytics');
const { trackEvent } = useAnalytics();

it('performs case-insensitive filtering', () => {
expect(filterNotes(testNotes, 'IMPORTANT')).toHaveLength(1);
expect(filterNotes(testNotes, 'Personal')).toHaveLength(2);
expect(filterNotes(testNotes, 'DEVELOPMENT')).toHaveLength(1);
});
// Simulate search tracking
const searchQuery = 'business';
const resultCount = 2;

it('handles partial matches correctly', () => {
const result = filterNotes(testNotes, 'review');
expect(result).toHaveLength(1); // Matches "Code Review" title and "Review implementation" body (same note)
expect(result[0].NoteId).toBe('3');
trackEvent('notes_search', {
searchQuery,
resultCount,
});

it('returns empty array for non-matching queries', () => {
expect(filterNotes(testNotes, 'nonexistent')).toHaveLength(0);
expect(filterNotes(testNotes, 'xyz123')).toHaveLength(0);
expect(mockTrackEvent).toHaveBeenCalledWith('notes_search', {
searchQuery: 'business',
resultCount: 2,
});
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Rename to “notes_searched” to match implementation

Update test to use the emitted event name.

Apply this diff:

-    trackEvent('notes_search', {
+    trackEvent('notes_searched', {
       searchQuery,
       resultCount,
     });
 
-    expect(mockTrackEvent).toHaveBeenCalledWith('notes_search', {
+    expect(mockTrackEvent).toHaveBeenCalledWith('notes_searched', {
       searchQuery: 'business',
       resultCount: 2,
     });
📝 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
it('tracks search analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { trackEvent } = useAnalytics();
it('performs case-insensitive filtering', () => {
expect(filterNotes(testNotes, 'IMPORTANT')).toHaveLength(1);
expect(filterNotes(testNotes, 'Personal')).toHaveLength(2);
expect(filterNotes(testNotes, 'DEVELOPMENT')).toHaveLength(1);
});
// Simulate search tracking
const searchQuery = 'business';
const resultCount = 2;
it('handles partial matches correctly', () => {
const result = filterNotes(testNotes, 'review');
expect(result).toHaveLength(1); // Matches "Code Review" title and "Review implementation" body (same note)
expect(result[0].NoteId).toBe('3');
trackEvent('notes_search', {
searchQuery,
resultCount,
});
it('returns empty array for non-matching queries', () => {
expect(filterNotes(testNotes, 'nonexistent')).toHaveLength(0);
expect(filterNotes(testNotes, 'xyz123')).toHaveLength(0);
expect(mockTrackEvent).toHaveBeenCalledWith('notes_search', {
searchQuery: 'business',
resultCount: 2,
});
});
it('tracks search analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { trackEvent } = useAnalytics();
// Simulate search tracking
const searchQuery = 'business';
const resultCount = 2;
trackEvent('notes_searched', {
searchQuery,
resultCount,
});
expect(mockTrackEvent).toHaveBeenCalledWith('notes_searched', {
searchQuery: 'business',
resultCount: 2,
});
});
🤖 Prompt for AI Agents
src/app/(app)/__tests__/notes.test.tsx around lines 58 to 75: the test asserts
the analytics event using the wrong event name 'notes_search' but the
implementation emits 'notes_searched'; update the test to call and expect
'notes_searched' instead (replace both trackEvent invocation and the expect
assertion to use 'notes_searched') so the test matches the actual emitted event
name.

Comment on lines +103 to +120
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');

// Simulate initial load and refresh
mockRefresh(); // Initial load
mockRefresh(); // User refresh
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();

expect(refreshCount).toBe(2);
// Simulate refresh tracking
trackEvent('notes_refresh', {
noteCount: notes.length,
hasSearchQuery: false,
});

it('validates search state management concept', () => {
// Mock search state
let searchQuery = '';
const setSearchQuery = (query: string) => {
searchQuery = query;
};
expect(mockTrackEvent).toHaveBeenCalledWith('notes_refresh', {
noteCount: 0,
hasSearchQuery: false,
});
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Rename to “notes_refreshed” to match implementation

Align with the Notes screen’s event name.

Apply this diff:

-    trackEvent('notes_refresh', {
+    trackEvent('notes_refreshed', {
       noteCount: notes.length,
       hasSearchQuery: false,
     });
 
-    expect(mockTrackEvent).toHaveBeenCalledWith('notes_refresh', {
+    expect(mockTrackEvent).toHaveBeenCalledWith('notes_refreshed', {
       noteCount: 0,
       hasSearchQuery: false,
     });
📝 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
it('tracks refresh analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');
// Simulate initial load and refresh
mockRefresh(); // Initial load
mockRefresh(); // User refresh
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();
expect(refreshCount).toBe(2);
// Simulate refresh tracking
trackEvent('notes_refresh', {
noteCount: notes.length,
hasSearchQuery: false,
});
it('validates search state management concept', () => {
// Mock search state
let searchQuery = '';
const setSearchQuery = (query: string) => {
searchQuery = query;
};
expect(mockTrackEvent).toHaveBeenCalledWith('notes_refresh', {
noteCount: 0,
hasSearchQuery: false,
});
});
it('tracks refresh analytics event with correct data', () => {
const { useAnalytics } = require('@/hooks/use-analytics');
const { useNotesStore } = require('@/stores/notes/store');
const { trackEvent } = useAnalytics();
const { notes } = useNotesStore();
// Simulate refresh tracking
trackEvent('notes_refreshed', {
noteCount: notes.length,
hasSearchQuery: false,
});
expect(mockTrackEvent).toHaveBeenCalledWith('notes_refreshed', {
noteCount: 0,
hasSearchQuery: false,
});
});
🤖 Prompt for AI Agents
In src/app/(app)/__tests__/notes.test.tsx around lines 103 to 120, the test uses
the wrong event name 'notes_refresh' while the implementation emits
'notes_refreshed'; update the test so both the trackEvent invocation and the
expectation use 'notes_refreshed' (i.e., replace 'notes_refresh' with
'notes_refreshed' in the simulated trackEvent call and the toHaveBeenCalledWith
assertion) so the test matches the actual implementation.

Comment on lines +68 to +73
trackEvent('map_viewed', {
timestamp: new Date().toISOString(),
isMapLocked: location.isMapLocked,
hasLocation: !!(location.latitude && location.longitude),
});

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t truthiness-check numeric coordinates; 0 is valid and currently treated as “no location”

Using expressions like location.latitude && location.longitude and !!(lat && lon) breaks at (0, 0). This causes:

  • Incorrect hasLocation analytics
  • Suppressed UI (PointAnnotation and recenter button won’t render)
  • Camera not resetting/recentering in valid 0/0 cases

Switch to null/undefined checks.

Apply this diff:

@@
-      trackEvent('map_viewed', {
-        timestamp: new Date().toISOString(),
-        isMapLocked: location.isMapLocked,
-        hasLocation: !!(location.latitude && location.longitude),
-      });
+      trackEvent('map_viewed', {
+        timestamp: new Date().toISOString(),
+        isMapLocked: location.isMapLocked,
+        hasLocation: location.latitude != null && location.longitude != null,
+      });
@@
-      if (isMapReady && location.latitude && location.longitude) {
+      if (isMapReady && location.latitude != null && location.longitude != null) {
@@
-    if (isMapReady && location.latitude && location.longitude) {
+    if (isMapReady && location.latitude != null && location.longitude != null) {
@@
-      if (isMapReady && location.latitude && location.longitude) {
+      if (isMapReady && location.latitude != null && location.longitude != null) {
@@
-  const handleRecenterMap = () => {
-    if (location.latitude && location.longitude) {
+  const handleRecenterMap = () => {
+    if (location.latitude != null && location.longitude != null) {
@@
-  const showRecenterButton = !location.isMapLocked && hasUserMovedMap && location.latitude && location.longitude;
+  const showRecenterButton = !location.isMapLocked && hasUserMovedMap && location.latitude != null && location.longitude != null;
@@
-              {location.latitude && location.longitude && (
+              {location.latitude != null && location.longitude != null && (

Also applies to: 78-104, 134-164, 167-190, 228-252, 307-309, 345-372

🤖 Prompt for AI Agents
In src/app/(app)/map.tsx around lines 68-73 (and similarly at 78-104, 134-164,
167-190, 228-252, 307-309, 345-372), the code uses truthiness checks like
location.latitude && location.longitude or !!(lat && lon) which treat 0 as falsy
and break valid (0,0) coordinates; replace those truthiness checks with explicit
null/undefined checks (e.g., check latitude != null && longitude != null or
latitude !== undefined && longitude !== undefined) wherever hasLocation or
conditional rendering/logic depends on coordinates so that 0 is treated as a
valid value and PointAnnotation, recentering, analytics, and camera logic behave
correctly.

Comment on lines +59 to +63
trackEvent('settings_login_info_updated', {
timestamp: new Date().toISOString(),
username: data.username,
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Stop sending raw usernames to analytics (PII leak)

Sending the entered username is personally identifiable and unnecessary for telemetry. Replace it with a non-identifying flag.

Apply this diff to remove the PII:

-    trackEvent('settings_login_info_updated', {
-      timestamp: new Date().toISOString(),
-      username: data.username,
-    });
+    trackEvent('settings_login_info_updated', {
+      timestamp: new Date().toISOString(),
+      usernamePresent: Boolean(data.username),
+    });
📝 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
trackEvent('settings_login_info_updated', {
timestamp: new Date().toISOString(),
username: data.username,
});
trackEvent('settings_login_info_updated', {
timestamp: new Date().toISOString(),
usernamePresent: Boolean(data.username),
});
🤖 Prompt for AI Agents
In src/app/(app)/settings.tsx around lines 59 to 63, stop sending the raw
username (PII) to analytics; replace the username field with a non-identifying
flag such as a boolean or coarse bucket. Remove data.username from the
trackEvent payload and instead send a field like "username_present" set to
!!data.username (or a simple length bucket like "username_length_bucket":
"0","1-3","4+"), so telemetry records presence/size without exposing the actual
username.

Comment on lines +280 to +283
NotesCount: 2,
ImgagesCount: 1,
FileCount: 3,
Latitude: '40.7128',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Typo in mockCall breaks images analytics/count assertions

The mock data uses ImgagesCount instead of the expected ImagesCount. This will likely lead to imagesCount being undefined/0 in the component and cause analytics assertions to fail.

Apply this diff to fix the typo:

-    ImgagesCount: 1,
+    ImagesCount: 1,
📝 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
NotesCount: 2,
ImgagesCount: 1,
FileCount: 3,
Latitude: '40.7128',
NotesCount: 2,
ImagesCount: 1,
FileCount: 3,
Latitude: '40.7128',
🤖 Prompt for AI Agents
In src/app/call/__tests__/[id].test.tsx around lines 280 to 283, the mock call
object has a typo property "ImgagesCount" which prevents the component from
reading the expected "ImagesCount"; rename the property to "ImagesCount"
(preserve value) in the mock data so the component/analytics receive the correct
images count.

Comment on lines +177 to +197
it('should handle useFocusEffect callback properly', () => {
// Test that useFocusEffect can be called with a callback
let focusCallback: (() => void) | undefined;
mockUseFocusEffect.mockImplementation((callback: () => void) => {
focusCallback = callback;
});

// This simulates what happens in the component
const { useAnalytics } = require('@/hooks/use-analytics');
const { trackEvent } = useAnalytics();

// Simulate component registering focus callback
mockUseFocusEffect(() => {
trackEvent('call_detail_viewed', {
timestamp: new Date().toISOString(),
callId: '123',
});
});

expect(mockUseFocusEffect).toHaveBeenCalledWith(expect.any(Function));
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Actually invoke the focus callback and assert telemetry

Currently this only asserts registration. Call the captured callback and verify trackEvent is fired to validate the flow.

Apply this diff:

-    it('should handle useFocusEffect callback properly', () => {
+    it('should handle useFocusEffect callback properly', () => {
       // Test that useFocusEffect can be called with a callback
       let focusCallback: (() => void) | undefined;
       mockUseFocusEffect.mockImplementation((callback: () => void) => {
         focusCallback = callback;
       });
 
       // This simulates what happens in the component
       const { useAnalytics } = require('@/hooks/use-analytics');
       const { trackEvent } = useAnalytics();
       
       // Simulate component registering focus callback
       mockUseFocusEffect(() => {
         trackEvent('call_detail_viewed', {
           timestamp: new Date().toISOString(),
           callId: '123',
         });
       });
 
-      expect(mockUseFocusEffect).toHaveBeenCalledWith(expect.any(Function));
+      expect(mockUseFocusEffect).toHaveBeenCalledWith(expect.any(Function));
+      // Now simulate focus
+      focusCallback && focusCallback();
+      expect(mockTrackEvent).toHaveBeenCalledWith('call_detail_viewed', expect.objectContaining({
+        callId: '123',
+        timestamp: expect.any(String),
+      }));
     });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/call/__tests__/analytics-integration.test.ts around lines 177 to 197,
the test only asserts that useFocusEffect was registered but doesn't actually
invoke the captured callback or verify telemetry; modify the test to call the
captured focusCallback (guarding that it's defined) and assert that the mocked
trackEvent was called with the expected event name and payload (or at least
called once), e.g., after mockUseFocusEffect assigns focusCallback, invoke
focusCallback() and add an expect(trackEvent).toHaveBeenCalledWith(...) or
expect(trackEvent).toHaveBeenCalled() to validate the flow.

@ucswift
Copy link
Member Author

ucswift commented Aug 21, 2025

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit f5eedac into master Aug 21, 2025
9 of 12 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants