Skip to content

Track expense created events in Sentry per request type#90388

Open
Julesssss wants to merge 2 commits into
mainfrom
jules-trackExpenseCreated
Open

Track expense created events in Sentry per request type#90388
Julesssss wants to merge 2 commits into
mainfrom
jules-trackExpenseCreated

Conversation

@Julesssss
Copy link
Copy Markdown
Contributor

Explanation of Change

Adds a per-request-type Sentry transaction whenever a distance expense is created, so we can answer "how many users used the odometer flow / each distance mode" without relying on screen-load proxies. Today, switching distance tabs inside Money_Request_Distance_Create produces no Sentry transaction (TopTab transitions aren't tracked by reactNavigationIntegration), and the deep-link Money_Request_Step_Distance_* routes only fire when reached from edit/split/receipt flows — neither captures the in-create-flow creation event.

A new leaf helper src/libs/telemetry/emitExpenseCreated.ts emits a forced Sentry transaction named ExpenseCreated.${iouRequestType} with op expense.created. It's called from the non-split branch of createDistanceRequest in src/libs/actions/IOU/Split.ts, right after the optimistic transaction is built. The split branch is structurally outside the call site, so split flows are not tracked.

Once deployed, queries like the following give per-mode unique-user counts:

project:app transaction:"ExpenseCreated.distance-odometer"
project:app transaction:"ExpenseCreated.distance-*"

Per-mode names emerge automatically from transaction.iouRequestType: distance-map, distance-manual, distance-gps, distance-odometer.

The helper is intentionally generic — extending to per-diem / track / scan / manual single-expense flows in the future is one line at each API.write(WRITE_COMMANDS.CREATE_*) site.

Fixed Issues

$ N/A — telemetry follow-up to #87726

PROPOSAL: N/A

Tests

  1. Open the App and start creating a new distance expense.
  2. Switch to the Odometer tab, enter start/end readings + images, tap NextCreate.
  3. With Sentry dev forwarding enabled (or watching network requests to sentry.io), verify a transaction is emitted with name ExpenseCreated.distance-odometer and op expense.created.
  4. Repeat for Map, Manual, and GPS tabs — verify each emits its corresponding ExpenseCreated.distance-* transaction.
  5. Start a split distance expense (any mode) and create it. Verify no ExpenseCreated.* transaction is emitted (split branch intentionally skipped).
  6. Verify that no errors appear in the JS console.

Offline tests

Same as Tests. The emit fires before deferOrExecuteWrite queues the API call, so transactions are recorded for offline creates as well. The Sentry transport will buffer envelopes until network returns.

QA Steps

Same as Tests.

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above (N/A — pure telemetry)
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (split flow should NOT emit)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions
  • I included screenshots or videos for tests on all platforms (N/A — no UI changes)
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors
  • I followed proper code patterns (see Reviewing the code)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers (mirrors the existing Sentry.startInactiveSpan pattern used in src/libs/OptionsListUtils/index.ts)
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes
  • I verified all code is DRY (single leaf helper, single call site)
  • I verified any variables that can be defined as constants are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly (N/A)
  • If any new file was added I verified that the file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow (N/A — pure side-effect telemetry; no behavioural change to assert)
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps

@Julesssss Julesssss requested review from a team as code owners May 12, 2026 22:55
@melvin-bot melvin-bot Bot requested review from JmillsExpensify and NicolasBonet and removed request for a team May 12, 2026 22:55
@melvin-bot
Copy link
Copy Markdown

melvin-bot Bot commented May 12, 2026

@NicolasBonet Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

endSpan(CONST.TELEMETRY.SPAN_OPEN_CREATE_EXPENSE);
}, []);

const handleTabFocused = useCallback((tabName: SelectedTabRequest) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CLEAN-REACT-PATTERNS-0 (docs)

React Compiler is enabled and this file compiles successfully. The useCallback wrapping handleTabFocused is redundant because the compiler automatically memoizes closures based on their captured variables. Manual memoization adds maintenance overhead (dependency arrays) with no benefit.

Remove useCallback and define handleTabFocused as a plain function:

const handleTabFocused = (tabName: SelectedTabRequest) => {
    Sentry.startInactiveSpan({
        name: `${SCREENS.MONEY_REQUEST.DISTANCE_CREATE}.tab.${tabName}`,
        op: 'ui.tab.focus',
        forceTransaction: true,
    })?.end();
};

Reviewed at: 1de2933 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}
Sentry.startInactiveSpan({
name: `ExpenseCreated.${iouRequestType}`,
op: 'expense.created',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The strings 'expense.created' (op) and the 'ExpenseCreated.' prefix (name) are magic strings. The existing telemetry codebase consistently defines span identifiers as named constants in CONST.TELEMETRY (e.g., CONST.TELEMETRY.SPAN_SUBMIT_EXPENSE, CONST.TELEMETRY.SPAN_APP_STARTUP). Using inline strings here diverges from that established pattern and makes it harder to find all telemetry span definitions in one place.

Define these as constants in CONST.TELEMETRY and reference them here:

// In CONST.ts under TELEMETRY
SPAN_EXPENSE_CREATED: 'ExpenseCreated',
OP_EXPENSE_CREATED: 'expense.created',

Reviewed at: 1de2933 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const handleTabFocused = useCallback((tabName: SelectedTabRequest) => {
Sentry.startInactiveSpan({
name: `${SCREENS.MONEY_REQUEST.DISTANCE_CREATE}.tab.${tabName}`,
op: 'ui.tab.focus',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The string 'ui.tab.focus' is a magic string used as a Sentry span operation name. The rest of the telemetry codebase defines span identifiers as named constants in CONST.TELEMETRY. Using an inline string here makes it harder to discover and maintain telemetry instrumentation points.

Define this as a constant in CONST.TELEMETRY and reference it here:

// In CONST.ts under TELEMETRY
OP_TAB_FOCUS: 'ui.tab.focus',

Reviewed at: 1de2933 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/libs/Navigation/OnyxTabNavigator.tsx 75.80% <100.00%> (+0.80%) ⬆️
src/libs/actions/IOU/Split.ts 72.32% <100.00%> (+0.05%) ⬆️
src/libs/telemetry/emitExpenseCreated.ts 100.00% <100.00%> (ø)
src/pages/iou/request/DistanceRequestStartPage.tsx 0.00% <0.00%> (ø)
... and 219 files with indirect coverage changes

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.

1 participant