Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
5a2008a
Merge remote-tracking branch 'origin/v6' into kw/ref-native-frames-in…
krystofwoldrich Aug 9, 2024
10aa4b0
Merge branch 'v6' into kw/ref-native-frames-integration
krystofwoldrich Aug 9, 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@
});
```

- New Native Frames Integration ([#3996](https://github.com/getsentry/sentry-react-native/pull/3996))

```js
Sentry.init({
tracesSampleRate: 1.0,
enableNativeFramesTracking: true, // default true
});
```

## 5.28.0

### Fixes
Expand Down
7 changes: 7 additions & 0 deletions src/js/integrations/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
inboundFiltersIntegration,
mobileReplayIntegration,
modulesLoaderIntegration,
nativeFramesIntegration,
nativeLinkedErrorsIntegration,
nativeReleaseIntegration,
reactNativeErrorHandlersIntegration,
Expand Down Expand Up @@ -98,6 +99,12 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
options.enableTracing ||
typeof options.tracesSampleRate === 'number' ||
typeof options.tracesSampler === 'function';
if (hasTracingEnabled && options.enableAppStartTracking) {
integrations.push(appStartIntegration());
}
if (hasTracingEnabled && options.enableNativeFramesTracking) {
integrations.push(nativeFramesIntegration());
}
if (hasTracingEnabled && options.enableAutoPerformanceTracing) {
integrations.push(new ReactNativeTracing());
}
Expand Down
1 change: 1 addition & 0 deletions src/js/integrations/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { expoContextIntegration } from './expocontext';
export { spotlightIntegration } from './spotlight';
export { mobileReplayIntegration } from '../replay/mobilereplay';
export { appStartIntegration } from '../tracing/integrations/appStart';
export { nativeFramesIntegration } from '../tracing/integrations/nativeFrames';

export {
breadcrumbsIntegration,
Expand Down
10 changes: 9 additions & 1 deletion src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,18 @@ export interface BaseReactNativeOptions {
*
* Requires performance monitoring to be enabled.
*
* Default: true
* @default true
*/
enableAppStartTracking?: boolean;

/**
* Track the slow and frozen frames in the application. Enabling this options will add
* slow and frozen frames measurements to all created root spans (transactions).
*
* @default true
*/
enableNativeFramesTracking?: boolean;

/**
* Options which are in beta, or otherwise not guaranteed to be stable.
*/
Expand Down
1 change: 1 addition & 0 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const DEFAULT_OPTIONS: ReactNativeOptions = {
enableCaptureFailedRequests: false,
enableNdk: true,
enableAppStartTracking: true,
enableNativeFramesTracking: true,
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { spanToJSON } from '@sentry/core';
import type { Client, Event, Integration, Measurements, MeasurementUnit, Span } from '@sentry/types';
import { logger, timestampInSeconds } from '@sentry/utils';

import type { NativeFramesResponse } from '../NativeRNSentry';
import { isRootSpan } from '../utils/span';
import { NATIVE } from '../wrapper';
import type { NativeFramesResponse } from '../../NativeRNSentry';
import type { ReactNativeClientOptions } from '../../options';
import { isRootSpan } from '../../utils/span';
import { NATIVE } from '../../wrapper';

/**
* Timeout from the final native frames fetch to processing the associated transaction.
Expand Down Expand Up @@ -34,42 +35,64 @@ const MARGIN_OF_ERROR_SECONDS = 0.05;
/**
* Instrumentation to add native slow/frozen frames measurements onto transactions.
*/
export class NativeFramesInstrumentation implements Integration {
public name: string = 'NativeFramesInstrumentation';
export const nativeFramesIntegration = (): Integration => {
const name: string = 'NativeFrames';

/** The native frames at the finish time of the most recent span. */
private _lastSpanFinishFrames?: {
timestamp: number;
nativeFrames: NativeFramesResponse;
};
private _spanToNativeFramesAtStartMap: Map<string, NativeFramesResponse> = new Map();

public constructor() {
logger.log('[ReactNativeTracing] Native frames instrumentation initialized.');
}
let _lastSpanFinishFrames:
| {
timestamp: number;
nativeFrames: NativeFramesResponse;
}
| undefined = undefined;
const _spanToNativeFramesAtStartMap: Map<string, NativeFramesResponse> = new Map();

/**
* Hooks into the client start and end span events.
*/
public setup(client: Client): void {
client.on('spanStart', this._onSpanStart);
client.on('spanEnd', this._onSpanFinish);
}
const setup = (client: Client): void => {
const { enableNativeFramesTracking } = client.getOptions() as ReactNativeClientOptions;

if (enableNativeFramesTracking && !NATIVE.enableNative) {
// Do not enable native frames tracking if native is not available.
logger.warn(
'[ReactNativeTracing] NativeFramesTracking is not available on the Web, Expo Go and other platforms without native modules.',
);
return;
}

if (!enableNativeFramesTracking && NATIVE.enableNative) {
// Disable native frames tracking when native available and option is false.
NATIVE.disableNativeFramesTracking();
return;
}

if (!enableNativeFramesTracking) {
return;
}

NATIVE.enableNativeFramesTracking();

// TODO: Ensure other integrations like ReactNativeTracing and ReactNavigation create spans after all integration are setup.
client.on('spanStart', _onSpanStart);
client.on('spanEnd', _onSpanFinish);
logger.log('[ReactNativeTracing] Native frames instrumentation initialized.');
};

/**
* Adds frames measurements to an event. Called from a valid event processor.
* Awaits for finish frames if needed.
*/
public processEvent(event: Event): Promise<Event> {
return this._processEvent(event);
}
const processEvent = (event: Event): Promise<Event> => {
return _processEvent(event);
};

/**
* Fetches the native frames in background if the given span is a root span.
*
* @param {Span} rootSpan - The span that has started.
*/
private _onSpanStart = (rootSpan: Span): void => {
const _onSpanStart = (rootSpan: Span): void => {
if (!isRootSpan(rootSpan)) {
return;
}
Expand All @@ -87,7 +110,7 @@ export class NativeFramesInstrumentation implements Integration {
return;
}

this._spanToNativeFramesAtStartMap.set(rootSpan.spanContext().traceId, frames);
_spanToNativeFramesAtStartMap.set(rootSpan.spanContext().traceId, frames);
})
.then(undefined, error => {
logger.error(
Expand All @@ -101,9 +124,9 @@ export class NativeFramesInstrumentation implements Integration {
* Called on a span finish to fetch native frames to support transactions with trimEnd.
* Only to be called when a span does not have an end timestamp.
*/
private _onSpanFinish = (span: Span): void => {
const _onSpanFinish = (span: Span): void => {
if (isRootSpan(span)) {
return this._onTransactionFinish(span);
return _onTransactionFinish(span);
}

const timestamp = timestampInSeconds();
Expand All @@ -114,7 +137,7 @@ export class NativeFramesInstrumentation implements Integration {
return;
}

this._lastSpanFinishFrames = {
_lastSpanFinishFrames = {
timestamp,
nativeFrames: frames,
};
Expand All @@ -127,26 +150,26 @@ export class NativeFramesInstrumentation implements Integration {
/**
* To be called when a transaction is finished
*/
private _onTransactionFinish(span: Span): void {
this._fetchFramesForTransaction(span).then(undefined, (reason: unknown) => {
const _onTransactionFinish = (span: Span): void => {
_fetchFramesForTransaction(span).then(undefined, (reason: unknown) => {
logger.error(
`[NativeFrames] Error while fetching frames for root span start (${span.spanContext().spanId})`,
reason,
);
});
}
};

/**
* Returns the computed frames measurements and awaits for them if they are not ready yet.
*/
private async _getFramesMeasurements(
const _getFramesMeasurements = (
traceId: string,
finalEndTimestamp: number,
startFrames: NativeFramesResponse,
): Promise<FramesMeasurements | null> {
): Promise<FramesMeasurements | null> => {
if (_finishFrames.has(traceId)) {
logger.debug(`[NativeFrames] Native end frames already fetched for trace id (${traceId}).`);
return this._prepareMeasurements(traceId, finalEndTimestamp, startFrames);
return Promise.resolve(_prepareMeasurements(traceId, finalEndTimestamp, startFrames));
}

return new Promise(resolve => {
Expand All @@ -159,22 +182,22 @@ export class NativeFramesInstrumentation implements Integration {

_framesListeners.set(traceId, () => {
logger.debug(`[NativeFrames] Native end frames listener called for trace id (${traceId}).`);
resolve(this._prepareMeasurements(traceId, finalEndTimestamp, startFrames));
resolve(_prepareMeasurements(traceId, finalEndTimestamp, startFrames));

clearTimeout(timeout);
_framesListeners.delete(traceId);
});
});
}
};

/**
* Returns the computed frames measurements given ready data
*/
private _prepareMeasurements(
const _prepareMeasurements = (
traceId: string,
finalEndTimestamp: number, // The actual transaction finish time.
startFrames: NativeFramesResponse,
): FramesMeasurements | null {
): FramesMeasurements | null => {
let finalFinishFrames: NativeFramesResponse | undefined;

const finish = _finishFrames.get(traceId);
Expand All @@ -187,13 +210,13 @@ export class NativeFramesInstrumentation implements Integration {
logger.debug(`[NativeFrames] Using frames from root span end (traceId, ${traceId}).`);
finalFinishFrames = finish.nativeFrames;
} else if (
this._lastSpanFinishFrames &&
Math.abs(this._lastSpanFinishFrames.timestamp - finalEndTimestamp) < MARGIN_OF_ERROR_SECONDS
_lastSpanFinishFrames &&
Math.abs(_lastSpanFinishFrames.timestamp - finalEndTimestamp) < MARGIN_OF_ERROR_SECONDS
) {
// Fallback to the last span finish if it is within the margin of error of the actual finish timestamp.
// This should be the case for trimEnd.
logger.debug(`[NativeFrames] Using native frames from last span end (traceId, ${traceId}).`);
finalFinishFrames = this._lastSpanFinishFrames.nativeFrames;
finalFinishFrames = _lastSpanFinishFrames.nativeFrames;
} else {
logger.warn(
`[NativeFrames] Frames were collected within larger than margin of error delay for traceId (${traceId}). Dropping the inaccurate values.`,
Expand Down Expand Up @@ -228,18 +251,18 @@ export class NativeFramesInstrumentation implements Integration {
}

return measurements;
}
};

/**
* Fetch finish frames for a transaction at the current time. Calls any awaiting listeners.
*/
private async _fetchFramesForTransaction(span: Span): Promise<void> {
const _fetchFramesForTransaction = async (span: Span): Promise<void> => {
const traceId = spanToJSON(span).trace_id;
if (!traceId) {
return;
}

const startFrames = this._spanToNativeFramesAtStartMap.get(span.spanContext().traceId);
const startFrames = _spanToNativeFramesAtStartMap.get(span.spanContext().traceId);

// This timestamp marks when the finish frames were retrieved. It should be pretty close to the transaction finish.
const timestamp = timestampInSeconds();
Expand All @@ -255,13 +278,13 @@ export class NativeFramesInstrumentation implements Integration {

_framesListeners.get(traceId)?.();

setTimeout(() => this._cancelEndFrames(span), FINAL_FRAMES_TIMEOUT_MS);
}
setTimeout(() => _cancelEndFrames(span), FINAL_FRAMES_TIMEOUT_MS);
};

/**
* On a finish frames failure, we cancel the await.
*/
private _cancelEndFrames(span: Span): void {
const _cancelEndFrames = (span: Span): void => {
const spanJSON = spanToJSON(span);
const traceId = spanJSON.trace_id;
if (!traceId) {
Expand All @@ -275,13 +298,13 @@ export class NativeFramesInstrumentation implements Integration {
`[NativeFrames] Native frames timed out for ${spanJSON.op} transaction ${spanJSON.description}. Not adding native frames measurements.`,
);
}
}
};

/**
* Adds frames measurements to an event. Called from a valid event processor.
* Awaits for finish frames if needed.
*/
private async _processEvent(event: Event): Promise<Event> {
const _processEvent = async (event: Event): Promise<Event> => {
if (
event.type !== 'transaction' ||
!event.transaction ||
Expand All @@ -295,16 +318,16 @@ export class NativeFramesInstrumentation implements Integration {

const traceOp = event.contexts.trace.op;
const traceId = event.contexts.trace.trace_id;
const startFrames = this._spanToNativeFramesAtStartMap.get(traceId);
this._spanToNativeFramesAtStartMap.delete(traceId);
const startFrames = _spanToNativeFramesAtStartMap.get(traceId);
_spanToNativeFramesAtStartMap.delete(traceId);
if (!startFrames) {
logger.warn(
`[NativeFrames] Start frames of transaction ${event.transaction} (eventId, ${event.event_id}) are missing, but it already ended.`,
);
return event;
}

const measurements = await this._getFramesMeasurements(traceId, event.timestamp, startFrames);
const measurements = await _getFramesMeasurements(traceId, event.timestamp, startFrames);

if (!measurements) {
logger.log(
Expand All @@ -329,5 +352,11 @@ export class NativeFramesInstrumentation implements Integration {
_finishFrames.delete(traceId);

return event;
}
}
};

return {
name,
setup,
processEvent,
};
};
Loading