Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(6) fix(tracing): ReactNativeTracing and initial navigation spans have to be created after integrations setup #4000

Merged
merged 44 commits into from
Aug 14, 2024
Merged
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c3dcfd3
chore(tracing): extract app start to a standalone integration
krystofwoldrich Jun 3, 2024
88d6988
Merge branch 'v6' into kw/ref-app-start-integration
krystofwoldrich Jun 11, 2024
fe53af5
fix merge
krystofwoldrich Jun 11, 2024
d4ac89f
fix spans, app start is now reported to Sentry
krystofwoldrich Jun 11, 2024
7c5dcaa
fix uikit init and minimal instrumentation edge cases
krystofwoldrich Jun 12, 2024
0b42472
update js docs
krystofwoldrich Jun 12, 2024
9765eaf
Merge branch 'v6' into kw/ref-app-start-integration
krystofwoldrich Aug 4, 2024
b9e9e9a
Add App Start Integration tests
krystofwoldrich Aug 5, 2024
b334931
Remove app start test from react native tracing
krystofwoldrich Aug 5, 2024
4d16787
clean up app start tests
krystofwoldrich Aug 5, 2024
4d84922
fix test affected by the app start extraction
krystofwoldrich Aug 5, 2024
0ff2020
Add standalone app start
krystofwoldrich Aug 5, 2024
b1eab51
fix
krystofwoldrich Aug 5, 2024
6d1cd70
ref(tracing): Extract NativeFrames as standalone integration
krystofwoldrich Aug 5, 2024
5eaaad2
Add integration handling test
krystofwoldrich Aug 6, 2024
eba6820
Merge branch 'kw/ref-app-start-integration' into kw/ref-native-frames…
krystofwoldrich Aug 6, 2024
91c1eb8
clean up integrations tests
krystofwoldrich Aug 6, 2024
db70c09
move native frames tests
krystofwoldrich Aug 6, 2024
e199244
add changelog
krystofwoldrich Aug 6, 2024
2956344
Merge branch 'kw/ref-app-start-integration' into kw/ref-native-frames…
krystofwoldrich Aug 6, 2024
adb53fc
fix
krystofwoldrich Aug 6, 2024
699fda7
move the app start test to tracing
krystofwoldrich Aug 6, 2024
7a386ab
Merge branch 'kw/ref-app-start-integration' into kw/ref-native-frames…
krystofwoldrich Aug 6, 2024
ad98ac0
fix tests
krystofwoldrich Aug 6, 2024
f2b9abe
add changelog
krystofwoldrich Aug 6, 2024
89b2354
ref(tracing): Extract Stall Tracking to a standalone integration
krystofwoldrich Aug 6, 2024
4f3ca7b
misc(tracing): Remove ReactNativeTracing deprecated options
krystofwoldrich Aug 6, 2024
5c12e5c
fix changelog
krystofwoldrich Aug 6, 2024
a111bdf
ref(tracing): Extract UserInteractionTracing as standalone interaction
krystofwoldrich Aug 6, 2024
92e04ee
Apply suggestions from code review
krystofwoldrich Aug 7, 2024
1168d4e
Revert "fix changelog"
krystofwoldrich Aug 7, 2024
a21e83d
Revert "misc(tracing): Remove ReactNativeTracing deprecated options"
krystofwoldrich Aug 7, 2024
af4c453
tests
krystofwoldrich Aug 7, 2024
3d33c02
fix tests
krystofwoldrich Aug 7, 2024
c71b900
fix tests
krystofwoldrich Aug 7, 2024
e632d53
Merge commit 'c71b9003a8934eca268d524732ab337325bc238d' into kw/ref-u…
krystofwoldrich Aug 7, 2024
474ee37
misc(tracing): Remove ReactNativeTracing deprecated options
krystofwoldrich Aug 6, 2024
07d9f00
fix changelog
krystofwoldrich Aug 6, 2024
6386062
refactor react native tracing to new function style integration
krystofwoldrich Aug 7, 2024
3002393
update changelog
krystofwoldrich Aug 7, 2024
2bbb93a
fix test, changelog and samples
krystofwoldrich Aug 8, 2024
9dd899e
fix(tracing): ReactNativeTracing and initial navigation spans have to…
krystofwoldrich Aug 8, 2024
5d98ded
fix background test
krystofwoldrich Aug 9, 2024
ecd26ed
Merge branch 'v6' into kw/tracing-integrations-independent-order
krystofwoldrich Aug 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add standalone app start
  • Loading branch information
krystofwoldrich committed Aug 5, 2024
commit 0ff2020ddef02a65561916c541aca2e933cf32f8
5 changes: 4 additions & 1 deletion samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ Sentry.init({
return event;
},
beforeSendTransaction(event) {
logWithoutTracing('Transaction beforeSend:', event.event_id);
logWithoutTracing('Transaction beforeSend:', event.event_id, event);
return event;
},
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
@@ -88,6 +88,9 @@ Sentry.init({
maskAllVectors: true,
// maskAllText: false,
}),
Sentry.appStartIntegration({
standalone: false,
}),
);
return integrations.filter(i => i.name !== 'Dedupe');
},
143 changes: 132 additions & 11 deletions src/js/tracing/integrations/appStart.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
/* eslint-disable complexity */
import type { Event, Integration, SpanJSON, TransactionEvent } from '@sentry/types';
import { logger } from '@sentry/utils';
import {
getCapturedScopesOnSpan,
getClient,
getCurrentScope,
SentryNonRecordingSpan,
startInactiveSpan,
} from '@sentry/core';
import type { Client, Event, Integration, SpanJSON, TransactionEvent } from '@sentry/types';
import { logger, timestampInSeconds } from '@sentry/utils';

import {
APP_START_COLD as APP_START_COLD_MEASUREMENT,
APP_START_WARM as APP_START_WARM_MEASUREMENT,
} from '../../measurements';
import type { NativeAppStartResponse } from '../../NativeRNSentry';
import type { ReactNativeClientOptions } from '../../options';
import { convertSpanToTransaction, setEndTimeValue } from '../../utils/span';
import { NATIVE } from '../../wrapper';
import {
APP_START_COLD as APP_START_COLD_OP,
APP_START_WARM as APP_START_WARM_OP,
UI_LOAD as UI_LOAD_OP,
} from '../ops';
import { ReactNativeTracing } from '../reactnativetracing';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes';
import { createChildSpanJSON, createSpanJSON, getBundleStartTimestampMs } from '../utils';

const INTEGRATION_NAME = 'AppStart';

export type AppStartIntegration = Integration & {
captureStandaloneAppStart: () => Promise<void>;
};

/**
* We filter out app start more than 60s.
* This could be due to many different reasons.
@@ -28,37 +42,97 @@ const MAX_APP_START_DURATION_MS = 60_000;
/** We filter out App starts which timestamp is 60s and more before the transaction start */
const MAX_APP_START_AGE_MS = 60_000;

/** App Start transaction name */
const APP_START_TX_NAME = 'App Start';

let recordedAppStartEndTimestampMs: number | undefined = undefined;
let rootComponentCreationTimestampMs: number | undefined = undefined;

/**
* Records the application start end.
* Used automatically by `Sentry.wrap`.
* Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`.
*/
export const setAppStartEndTimestampMs = (timestampMs: number): void => {
recordedAppStartEndTimestampMs && logger.warn('Overwriting already set app start.');
recordedAppStartEndTimestampMs = timestampMs;
};
export async function captureAppStart(): Promise<void> {
const client = getClient();
if (!client) {
logger.warn('[AppStart] Could not capture App Start, missing client.');
return;
}

_setAppStartEndTimestampMs(timestampInSeconds() * 1000);
await client.getIntegrationByName<AppStartIntegration>(INTEGRATION_NAME)?.captureStandaloneAppStart();
}

/**
* Sets the root component first constructor call timestamp.
* Used automatically by `Sentry.wrap`.
* Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`.
*/
export function setRootComponentCreationTimestampMs(timestampMs: number): void {
recordedAppStartEndTimestampMs &&
logger.warn('Setting Root component creation timestamp after app start end is set.');
rootComponentCreationTimestampMs && logger.warn('Overwriting already set root component creation timestamp.');
rootComponentCreationTimestampMs = timestampMs;
}

/**
* For internal use only.
*
* @private
*/
export const _setAppStartEndTimestampMs = (timestampMs: number): void => {
recordedAppStartEndTimestampMs && logger.warn('Overwriting already set app start.');
recordedAppStartEndTimestampMs = timestampMs;
};

/**
* For testing purposes only.
*
* @private
*/
export function _clearRootComponentCreationTimestampMs(): void {
rootComponentCreationTimestampMs = undefined;
}

/**
* Adds AppStart spans from the native layer to the transaction event.
*/
export const appStartIntegration = (): Integration => {
export const appStartIntegration = ({
standalone: standaloneUserOption,
}: {
/**
* Should the integration send App Start as a standalone root span (transaction)?
* If false, App Start will be added as a child span to the first transaction.
*
* @default false
*/
standalone?: boolean;
} = {}): AppStartIntegration => {
let _client: Client | undefined = undefined;
let standalone = standaloneUserOption;
let isEnabled = true;
let appStartDataFlushed = false;

const setup = (client: Client): void => {
_client = client;
const clientOptions = client.getOptions() as ReactNativeClientOptions;

const { enableAppStartTracking } = clientOptions;
if (!enableAppStartTracking) {
isEnabled = false;
logger.warn('[AppStart] App start tracking is disabled.');
}
};

const afterAllSetup = (client: Client): void => {
if (standaloneUserOption === undefined) {
// If not user defined, set based on the routing instrumentation presence
standalone = !client.getIntegrationByName<ReactNativeTracing>(ReactNativeTracing.id)?.options
.routingInstrumentation;
}
};

const processEvent = async (event: Event): Promise<Event> => {
if (appStartDataFlushed) {
// App start data is only relevant for the first transaction
if (!isEnabled || standalone) {
return event;
}

@@ -72,7 +146,51 @@ export const appStartIntegration = (): Integration => {
return event;
};

async function captureStandaloneAppStart(): Promise<void> {
if (!standalone) {
logger.debug(
'[AppStart] App start tracking is enabled. App start will be added to the first transaction as a child span.',
);
return;
}

logger.debug('[AppStart] App start tracking standalone root span (transaction).');

const span = startInactiveSpan({
forceTransaction: true,
name: APP_START_TX_NAME,
op: UI_LOAD_OP,
});
if (span instanceof SentryNonRecordingSpan) {
// Tracing is disabled or the transaction was sampled
return;
}

setEndTimeValue(span, timestampInSeconds());
_client.emit('spanEnd', span);

const event = convertSpanToTransaction(span);
if (!event) {
logger.warn('[AppStart] Failed to convert App Start span to transaction.');
return;
}

await attachAppStartToTransactionEvent(event);
if (!event.spans || event.spans.length === 0) {
// No spans were added to the transaction, so we don't need to send it
return;
}

const scope = getCapturedScopesOnSpan(span).scope || getCurrentScope();
scope.captureEvent(event);
}

async function attachAppStartToTransactionEvent(event: TransactionEvent): Promise<void> {
if (appStartDataFlushed) {
// App start data is only relevant for the first transaction
return;
}

if (!event.contexts || !event.contexts.trace) {
logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.');
return;
@@ -185,7 +303,10 @@ export const appStartIntegration = (): Integration => {

return {
name: INTEGRATION_NAME,
setup,
afterAllSetup,
processEvent,
captureStandaloneAppStart,
};
};

6 changes: 3 additions & 3 deletions src/js/tracing/reactnativeprofiler.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { getClient, Profiler } from '@sentry/react';
import { timestampInSeconds } from '@sentry/utils';

import { createIntegration } from '../integrations/factory';
import { setAppStartEndTimestampMs, setRootComponentCreationTimestampMs } from '../tracing/integrations/appStart';
import { captureAppStart, setRootComponentCreationTimestampMs } from '../tracing/integrations/appStart';

const ReactNativeProfilerGlobalState = {
appStartReported: false,
@@ -44,7 +44,7 @@ export class ReactNativeProfiler extends Profiler {
}

client.addIntegration && client.addIntegration(createIntegration(this.name));

setAppStartEndTimestampMs(timestampInSeconds() * 1000);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
captureAppStart();
}
}
24 changes: 23 additions & 1 deletion src/js/utils/span.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getRootSpan, SentrySpan } from '@sentry/core';
import type { Span } from '@sentry/types';
import type { Span, TransactionEvent } from '@sentry/types';

/**
*
@@ -14,3 +14,25 @@ export function isSentrySpan(span: Span): span is SentrySpan {
export function isRootSpan(span: Span): boolean {
return span === getRootSpan(span);
}

const END_TIME_SCOPE_FIELD = '_endTime';
const CONVERT_SPAN_TO_TRANSACTION_FIELD = '_convertSpanToTransaction';

type SpanWithPrivate = Span & {
[END_TIME_SCOPE_FIELD]?: number;
[CONVERT_SPAN_TO_TRANSACTION_FIELD]?: () => TransactionEvent | undefined;
};

/**
*
*/
export function setEndTimeValue(span: Span, endTimestamp: number): void {
(span as SpanWithPrivate)['_endTime'] = endTimestamp;
}

/**
*
*/
export function convertSpanToTransaction(span: Span): TransactionEvent | undefined {
return (span as SpanWithPrivate)['_convertSpanToTransaction']?.();
}
Loading