Skip to content

Commit 24138d3

Browse files
antonisclaude
andauthored
fix(profiling): Fix app start transaction profile timestamp offset (#5962)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 09f1971 commit 24138d3

File tree

5 files changed

+179
-6
lines changed

5 files changed

+179
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
### Fixes
1212

13-
- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965)
13+
- Fix app start transaction profile offset by using the actual profiling start timestamp instead of the adjusted app start time ([#5962](https://github.com/getsentry/sentry-react-native/issues/5962))
14+
- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965))
1415
- Add `SENTRY_PROJECT_ROOT` env var to override project root in Xcode build phase scripts for monorepo setups ([#5961](https://github.com/getsentry/sentry-react-native/pull/5961))
1516

1617
### Features

packages/core/src/js/profiling/integration.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,17 @@ export function stopProfiling(
279279
return null;
280280
}
281281

282+
hermesProfileEvent.profilingStartTimestampNs = profileStartTimestampNs;
283+
282284
if (collectedProfiles.androidProfile) {
283285
const durationNs = profileEndTimestampNs - profileStartTimestampNs;
284-
return createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs);
286+
const androidProfile = createAndroidWithHermesProfile(
287+
hermesProfileEvent,
288+
collectedProfiles.androidProfile,
289+
durationNs,
290+
);
291+
androidProfile.profilingStartTimestampNs = profileStartTimestampNs;
292+
return androidProfile;
285293
} else if (collectedProfiles.nativeProfile) {
286294
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
287295
}

packages/core/src/js/profiling/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type HermesProfileEvent = {
1515
transaction: {
1616
active_thread_id: string;
1717
};
18+
profilingStartTimestampNs?: number;
1819
};
1920

2021
/*
@@ -31,6 +32,7 @@ export type AndroidCombinedProfileEvent = {
3132
android_api_level: number;
3233
duration_ns: string;
3334
active_thread_id: string;
35+
profilingStartTimestampNs?: number;
3436
};
3537

3638
/*

packages/core/src/js/profiling/utils.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,20 @@ export function enrichCombinedProfileWithEventContext(
8888
}
8989
}
9090

91+
const { profilingStartTimestampNs, ...profileWithoutInternalFields } = profile;
92+
9193
return {
92-
...profile,
94+
...profileWithoutInternalFields,
9395
event_id: profile_id,
9496
runtime: {
9597
name: 'hermes',
9698
version: '', // TODO: get hermes version
9799
},
98-
timestamp: event.start_timestamp ? new Date(event.start_timestamp * 1000).toISOString() : new Date().toISOString(),
100+
timestamp: profilingStartTimestampNs
101+
? new Date(profilingStartTimestampNs / 1e6).toISOString()
102+
: event.start_timestamp
103+
? new Date(event.start_timestamp * 1000).toISOString()
104+
: new Date().toISOString(),
99105
release: event.release || '',
100106
environment: event.environment || getDefaultEnvironment(),
101107
os: {
@@ -130,8 +136,10 @@ export function enrichAndroidProfileWithEventContext(
130136
profile: AndroidCombinedProfileEvent,
131137
event: Event,
132138
): AndroidProfileEvent | null {
139+
const { profilingStartTimestampNs, ...profileWithoutInternalFields } = profile;
140+
133141
return {
134-
...profile,
142+
...profileWithoutInternalFields,
135143
debug_meta: {
136144
images: getDebugMetadata(),
137145
},
@@ -152,7 +160,11 @@ export function enrichAndroidProfileWithEventContext(
152160

153161
profile_id,
154162

155-
timestamp: event.start_timestamp ? new Date(event.start_timestamp * 1000).toISOString() : new Date().toISOString(),
163+
timestamp: profilingStartTimestampNs
164+
? new Date(profilingStartTimestampNs / 1e6).toISOString()
165+
: event.start_timestamp
166+
? new Date(event.start_timestamp * 1000).toISOString()
167+
: new Date().toISOString(),
156168

157169
release: event.release || '',
158170
dist: event.dist || '',
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
jest.mock('../../src/js/utils/environment');
2+
jest.mock('../../src/js/profiling/debugid');
3+
4+
import type { Event } from '@sentry/core';
5+
6+
import type { AndroidCombinedProfileEvent, CombinedProfileEvent } from '../../src/js/profiling/types';
7+
8+
import { getDebugMetadata } from '../../src/js/profiling/debugid';
9+
import {
10+
enrichAndroidProfileWithEventContext,
11+
enrichCombinedProfileWithEventContext,
12+
} from '../../src/js/profiling/utils';
13+
import { getDefaultEnvironment } from '../../src/js/utils/environment';
14+
import { createMockMinimalValidAndroidProfile, createMockMinimalValidHermesProfileEvent } from './fixtures';
15+
16+
describe('enrichCombinedProfileWithEventContext', () => {
17+
beforeEach(() => {
18+
(getDefaultEnvironment as jest.Mock).mockReturnValue('production');
19+
(getDebugMetadata as jest.Mock).mockReturnValue([]);
20+
});
21+
22+
function createMockEvent(overrides?: Partial<Event>): Event {
23+
return {
24+
event_id: 'test-event-id',
25+
transaction: 'test-transaction',
26+
release: 'test-release',
27+
environment: 'test-env',
28+
start_timestamp: 1000,
29+
contexts: {
30+
trace: {
31+
trace_id: '12345678901234567890123456789012',
32+
},
33+
os: { name: 'iOS', version: '17.0' },
34+
device: {},
35+
},
36+
...overrides,
37+
};
38+
}
39+
40+
test('should use profilingStartTimestampNs for timestamp when available', () => {
41+
const profilingStartTimestampNs = 1500 * 1e9; // 1500 seconds in ns
42+
const profile: CombinedProfileEvent = {
43+
...createMockMinimalValidHermesProfileEvent(),
44+
profilingStartTimestampNs,
45+
};
46+
const event = createMockEvent({ start_timestamp: 1000 }); // earlier than profiling start
47+
48+
const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);
49+
50+
expect(result).not.toBeNull();
51+
expect(result!.timestamp).toBe(new Date(profilingStartTimestampNs / 1e6).toISOString());
52+
// Should NOT use event.start_timestamp
53+
expect(result!.timestamp).not.toBe(new Date(1000 * 1000).toISOString());
54+
});
55+
56+
test('should fall back to event.start_timestamp when profilingStartTimestampNs is not set', () => {
57+
const profile: CombinedProfileEvent = createMockMinimalValidHermesProfileEvent();
58+
const event = createMockEvent({ start_timestamp: 1000 });
59+
60+
const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);
61+
62+
expect(result).not.toBeNull();
63+
expect(result!.timestamp).toBe(new Date(1000 * 1000).toISOString());
64+
});
65+
66+
test('should not include profilingStartTimestampNs in the output', () => {
67+
const profile: CombinedProfileEvent = {
68+
...createMockMinimalValidHermesProfileEvent(),
69+
profilingStartTimestampNs: 1500 * 1e9,
70+
};
71+
const event = createMockEvent();
72+
73+
const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);
74+
75+
expect(result).not.toBeNull();
76+
expect(result).not.toHaveProperty('profilingStartTimestampNs');
77+
});
78+
});
79+
80+
describe('enrichAndroidProfileWithEventContext', () => {
81+
beforeEach(() => {
82+
(getDefaultEnvironment as jest.Mock).mockReturnValue('production');
83+
(getDebugMetadata as jest.Mock).mockReturnValue([]);
84+
});
85+
86+
function createMockEvent(overrides?: Partial<Event>): Event {
87+
return {
88+
event_id: 'test-event-id',
89+
transaction: 'test-transaction',
90+
release: 'test-release',
91+
environment: 'test-env',
92+
start_timestamp: 1000,
93+
contexts: {
94+
trace: {
95+
trace_id: '12345678901234567890123456789012',
96+
},
97+
os: { name: 'Android', version: '14' },
98+
device: {},
99+
},
100+
...overrides,
101+
};
102+
}
103+
104+
function createMockAndroidCombinedProfile(
105+
overrides?: Partial<AndroidCombinedProfileEvent>,
106+
): AndroidCombinedProfileEvent {
107+
const hermesProfileEvent = createMockMinimalValidHermesProfileEvent();
108+
return {
109+
platform: 'android',
110+
sampled_profile: createMockMinimalValidAndroidProfile().sampled_profile,
111+
js_profile: hermesProfileEvent.profile,
112+
android_api_level: 34,
113+
duration_ns: '1000000',
114+
active_thread_id: '123',
115+
...overrides,
116+
};
117+
}
118+
119+
test('should use profilingStartTimestampNs for timestamp when available', () => {
120+
const profilingStartTimestampNs = 1500 * 1e9;
121+
const profile = createMockAndroidCombinedProfile({ profilingStartTimestampNs });
122+
const event = createMockEvent({ start_timestamp: 1000 });
123+
124+
const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);
125+
126+
expect(result).not.toBeNull();
127+
expect(result!.timestamp).toBe(new Date(profilingStartTimestampNs / 1e6).toISOString());
128+
expect(result!.timestamp).not.toBe(new Date(1000 * 1000).toISOString());
129+
});
130+
131+
test('should fall back to event.start_timestamp when profilingStartTimestampNs is not set', () => {
132+
const profile = createMockAndroidCombinedProfile();
133+
const event = createMockEvent({ start_timestamp: 1000 });
134+
135+
const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);
136+
137+
expect(result).not.toBeNull();
138+
expect(result!.timestamp).toBe(new Date(1000 * 1000).toISOString());
139+
});
140+
141+
test('should not include profilingStartTimestampNs in the output', () => {
142+
const profile = createMockAndroidCombinedProfile({ profilingStartTimestampNs: 1500 * 1e9 });
143+
const event = createMockEvent();
144+
145+
const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);
146+
147+
expect(result).not.toBeNull();
148+
expect(result).not.toHaveProperty('profilingStartTimestampNs');
149+
});
150+
});

0 commit comments

Comments
 (0)