Skip to content

Commit 8f075ea

Browse files
authored
Merge pull request #90667 from margelo/@chrispader/add-startup-network-request-sentry-span
[No QA] feat: Add Sentry span for `OpenApp`/`ReconnectApp` network request on app startup
2 parents c1c7e6b + 4048dda commit 8f075ea

8 files changed

Lines changed: 72 additions & 21 deletions

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,7 @@ const CONST = {
20262026
// Span names
20272027
SPAN_OPEN_REPORT: 'ManualOpenReport',
20282028
SPAN_APP_STARTUP: 'ManualAppStartup',
2029+
SPAN_APP_STARTUP_NETWORK_REQUEST: 'ManualAppStartupNetworkRequest',
20292030
SPAN_NAVIGATE_TO_REPORTS: 'ManualNavigateToReports',
20302031
SPAN_NAVIGATE_TO_INBOX_TAB: 'ManualNavigateToInboxTab',
20312032
SPAN_OD_ND_TRANSITION: 'ManualOdNdTransition',
@@ -2113,6 +2114,7 @@ const CONST = {
21132114
ATTRIBUTE_IS_MULTI_SCAN: 'is_multi_scan',
21142115
ATTRIBUTE_SOURCE: 'source',
21152116
ATTRIBUTE_ODOMETER_IMAGE_TYPE: 'odometer_image_type',
2117+
ATTRIBUTE_DURATION_SINCE_NATIVE_APP_STARTUP_MS: 'duration_since_native_app_startup_ms',
21162118
/** Follow-up action after expense submit (action-based; used as submit_follow_up_action in span). */
21172119
SUBMIT_FOLLOW_UP_ACTION: {
21182120
DISMISS_MODAL_AND_OPEN_REPORT: 'dismiss_modal_and_open_report',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {WRITE_COMMANDS} from './API/types';
2+
3+
/**
4+
* One of these API requests is executed on every app startup.
5+
* We measure the duration from native app startup (before JS runtime is loaded)
6+
* to the completion of this request and send the span to Sentry.
7+
*/
8+
const APP_STARTUP_NETWORK_REQUEST = new Set<string>([WRITE_COMMANDS.OPEN_APP, WRITE_COMMANDS.RECONNECT_APP]);
9+
10+
export default APP_STARTUP_NETWORK_REQUEST;

src/libs/HttpUtils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {getCommandURL} from './ApiUtils';
1313
import HttpsError from './Errors/HttpsError';
1414
import {setLoadTestParameters} from './Network/LoadTestState';
1515
import prepareRequestPayload from './prepareRequestPayload';
16+
import markAppStartupNetworkRequestEnd from './telemetry/markAppStartupNetworkRequestEnd';
1617

1718
let shouldFailAllRequests = false;
1819
let shouldForceOffline = false;
@@ -64,6 +65,9 @@ function processHTTPRequest<TKey extends OnyxKey>(
6465
abortSignal: AbortSignal | undefined = undefined,
6566
): Promise<Response<TKey>> {
6667
const startTime = new Date().valueOf();
68+
69+
const command = url.match(APICommandRegex)?.[1];
70+
6771
return fetch(url, {
6872
// We hook requests to the same Controller signal, so we can cancel them all at once
6973
signal: abortSignal,
@@ -81,8 +85,8 @@ function processHTTPRequest<TKey extends OnyxKey>(
8185
}
8286

8387
// We are calculating the skew to minimize the delay when posting the messages
84-
const match = url.match(APICommandRegex)?.[1];
85-
if (match && addSkewList.has(match) && response.headers) {
88+
89+
if (command && addSkewList.has(command) && response.headers) {
8690
const dateHeaderValue = response.headers.get('Date');
8791
const serverTime = dateHeaderValue ? new Date(dateHeaderValue).valueOf() : new Date().valueOf();
8892
const endTime = new Date().valueOf();
@@ -162,7 +166,8 @@ function processHTTPRequest<TKey extends OnyxKey>(
162166
alertUser();
163167
}
164168
return response;
165-
});
169+
})
170+
.finally(() => markAppStartupNetworkRequestEnd(command));
166171
}
167172

168173
/**

src/libs/Middleware/SentryServerTiming.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type {SpanAttributes} from '@sentry/core';
12
import {WRITE_COMMANDS} from '@libs/API/types';
2-
import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
3+
import {cancelSpan, endSpanWithAttributes, startSpan} from '@libs/telemetry/activeSpans';
34
import CONST from '@src/CONST';
45
import type Middleware from './types';
56

@@ -67,11 +68,10 @@ const SentryServerTiming: Middleware = (response, request) => {
6768

6869
return response
6970
.then((data) => {
70-
const span = getSpan(spanId);
71-
span?.setAttributes({
71+
const attributes: SpanAttributes = {
7272
[CONST.TELEMETRY.ATTRIBUTE_JSON_CODE]: data?.jsonCode,
73-
});
74-
endSpan(spanId);
73+
};
74+
endSpanWithAttributes(spanId, attributes);
7575
return data;
7676
})
7777
.catch((error) => {

src/libs/telemetry/activeSpans.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import {SPAN_STATUS_OK} from '@sentry/core';
12
import type {SpanAttributeValue, StartSpanOptions} from '@sentry/core';
23
import * as Sentry from '@sentry/react-native';
34
import {AppState} from 'react-native';
45
import CONST from '@src/CONST';
56

67
type ActiveSpanEntry = {
78
span: ReturnType<typeof Sentry.startInactiveSpan>;
8-
startTime: number;
9+
startTimeForLog: number;
910
};
1011

1112
const activeSpans = new Map<string, ActiveSpanEntry>();
@@ -35,7 +36,15 @@ function startSpan(spanId: string, options: StartSpanOptions, extraOptions: Star
3536
if (extraOptions.minDuration) {
3637
span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_MIN_DURATION, extraOptions.minDuration);
3738
}
38-
activeSpans.set(spanId, {span, startTime: performance.now()});
39+
40+
let startTimeForLog: number;
41+
if (typeof options.startTime === 'number') {
42+
startTimeForLog = options.startTime;
43+
} else {
44+
startTimeForLog = performance.now();
45+
}
46+
47+
activeSpans.set(spanId, {span, startTimeForLog});
3948

4049
return span;
4150
}
@@ -46,11 +55,12 @@ function endSpan(spanId: string) {
4655
if (!entry) {
4756
return;
4857
}
49-
const {span, startTime} = entry;
58+
const {span, startTimeForLog} = entry;
5059
const now = performance.now();
51-
const durationMs = Math.round(now - startTime);
60+
const durationMs = Math.round(now - startTimeForLog);
5261
console.debug(`[Sentry][${spanId}] Ending span (${durationMs}ms)`, {spanId, durationMs, timestamp: now, attributes: Sentry.spanToJSON(span).data});
53-
span.setStatus({code: 1});
62+
span.setStatus({code: SPAN_STATUS_OK});
63+
5464
span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_FINISHED_MANUALLY, true);
5565
span.end();
5666
activeSpans.delete(spanId);
@@ -64,7 +74,7 @@ function cancelSpan(spanId: string) {
6474
entry.span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_CANCELED, true);
6575
// In Sentry there are only OK or ERROR status codes.
6676
// We treat canceled spans as OK, so we can properly track spans that are not finished at all (their status would be different)
67-
entry.span.setStatus({code: 1});
77+
entry.span.setStatus({code: SPAN_STATUS_OK});
6878
endSpan(spanId);
6979
}
7080

@@ -86,7 +96,7 @@ function getSpan(spanId: string) {
8696
return activeSpans.get(spanId)?.span;
8797
}
8898

89-
function endSpanWithAttributes(spanId: string, attributes: Record<string, SpanAttributeValue>) {
99+
function endSpanWithAttributes(spanId: string, attributes: Record<string, SpanAttributeValue | undefined>) {
90100
const span = getSpan(spanId);
91101
span?.setAttributes(attributes);
92102
endSpan(spanId);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import APP_STARTUP_NETWORK_REQUEST from '@libs/AppStartupNetworkRequest';
2+
import CONST from '@src/CONST';
3+
import {endSpanWithAttributes} from './activeSpans';
4+
5+
/** End the span for the app startup network request. */
6+
function markAppStartupNetworkRequestEnd(command: string | undefined): void {
7+
if (!command || !APP_STARTUP_NETWORK_REQUEST.has(command)) {
8+
return;
9+
}
10+
11+
endSpanWithAttributes(CONST.TELEMETRY.SPAN_APP_STARTUP_NETWORK_REQUEST, {
12+
[CONST.TELEMETRY.ATTRIBUTE_COMMAND]: command,
13+
});
14+
}
15+
16+
export default markAppStartupNetworkRequestEnd;

src/libs/telemetry/markOpenReportEnd.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import type {SpanAttributes} from '@sentry/core';
12
import {isOneTransactionReport, isReportTransactionThread} from '@libs/ReportUtils';
23
import CONST from '@src/CONST';
34
import type * as OnyxTypes from '@src/types/onyx';
4-
import {endSpan, getSpan} from './activeSpans';
5+
import {endSpanWithAttributes} from './activeSpans';
56

67
type MarkOpenReportEndOptions = {
78
warm?: boolean;
@@ -17,18 +18,19 @@ function markOpenReportEnd(report: OnyxTypes.Report, options: MarkOpenReportEndO
1718
const isOneTransactionThread = isOneTransactionReport(report);
1819

1920
const spanId = `${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`;
20-
const span = getSpan(spanId);
21-
span?.setAttributes({
21+
22+
const attributes: SpanAttributes = {
2223
[CONST.TELEMETRY.ATTRIBUTE_IS_TRANSACTION_THREAD]: isTransactionThread,
2324
[CONST.TELEMETRY.ATTRIBUTE_IS_ONE_TRANSACTION_REPORT]: isOneTransactionThread,
2425
[CONST.TELEMETRY.ATTRIBUTE_REPORT_TYPE]: type,
2526
[CONST.TELEMETRY.ATTRIBUTE_CHAT_TYPE]: chatType,
26-
});
27+
};
28+
2729
if (options.warm !== undefined) {
28-
span?.setAttribute(CONST.TELEMETRY.ATTRIBUTE_IS_WARM, options.warm);
30+
attributes[CONST.TELEMETRY.ATTRIBUTE_IS_WARM] = options.warm;
2931
}
3032

31-
endSpan(spanId);
33+
endSpanWithAttributes(spanId, attributes);
3234
}
3335

3436
export default markOpenReportEnd;

src/setup/telemetry/index.native.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export default function (): void {
2323
startTime: nativeAppStartTimeMs,
2424
});
2525

26+
startSpan(CONST.TELEMETRY.SPAN_APP_STARTUP_NETWORK_REQUEST, {
27+
name: CONST.TELEMETRY.SPAN_APP_STARTUP_NETWORK_REQUEST,
28+
op: CONST.TELEMETRY.SPAN_APP_STARTUP_NETWORK_REQUEST,
29+
startTime: nativeAppStartTimeMs,
30+
});
31+
2632
requestAnimationFrame(() => {
2733
// Use typeof guard — bare identifier throws ReferenceError if moduleInitPolyfill didn't run
2834
const initTimes = typeof __moduleInitTimes !== 'undefined' ? (__moduleInitTimes as Record<string, number>) : undefined;

0 commit comments

Comments
 (0)