Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@

### Fixes

<<<<<<< fix/profile-app-start-timestamp-offset
- 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))
=======
- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965)
>>>>>>> main
- 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))

### Features
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/js/profiling/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,17 @@ export function stopProfiling(
return null;
}

hermesProfileEvent.profilingStartTimestampNs = profileStartTimestampNs;

if (collectedProfiles.androidProfile) {
const durationNs = profileEndTimestampNs - profileStartTimestampNs;
return createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs);
const androidProfile = createAndroidWithHermesProfile(
hermesProfileEvent,
collectedProfiles.androidProfile,
durationNs,
);
androidProfile.profilingStartTimestampNs = profileStartTimestampNs;
return androidProfile;
} else if (collectedProfiles.nativeProfile) {
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/js/profiling/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type HermesProfileEvent = {
transaction: {
active_thread_id: string;
};
profilingStartTimestampNs?: number;
};

/*
Expand All @@ -31,6 +32,7 @@ export type AndroidCombinedProfileEvent = {
android_api_level: number;
duration_ns: string;
active_thread_id: string;
profilingStartTimestampNs?: number;
};

/*
Expand Down
20 changes: 16 additions & 4 deletions packages/core/src/js/profiling/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,20 @@ export function enrichCombinedProfileWithEventContext(
}
}

const { profilingStartTimestampNs, ...profileWithoutInternalFields } = profile;

return {
...profile,
...profileWithoutInternalFields,
event_id: profile_id,
runtime: {
name: 'hermes',
version: '', // TODO: get hermes version
},
timestamp: event.start_timestamp ? new Date(event.start_timestamp * 1000).toISOString() : new Date().toISOString(),
timestamp: profilingStartTimestampNs
? new Date(profilingStartTimestampNs / 1e6).toISOString()
: event.start_timestamp
? new Date(event.start_timestamp * 1000).toISOString()
: new Date().toISOString(),
release: event.release || '',
environment: event.environment || getDefaultEnvironment(),
os: {
Expand Down Expand Up @@ -130,8 +136,10 @@ export function enrichAndroidProfileWithEventContext(
profile: AndroidCombinedProfileEvent,
event: Event,
): AndroidProfileEvent | null {
const { profilingStartTimestampNs, ...profileWithoutInternalFields } = profile;

return {
...profile,
...profileWithoutInternalFields,
debug_meta: {
images: getDebugMetadata(),
},
Expand All @@ -152,7 +160,11 @@ export function enrichAndroidProfileWithEventContext(

profile_id,

timestamp: event.start_timestamp ? new Date(event.start_timestamp * 1000).toISOString() : new Date().toISOString(),
timestamp: profilingStartTimestampNs
? new Date(profilingStartTimestampNs / 1e6).toISOString()
: event.start_timestamp
? new Date(event.start_timestamp * 1000).toISOString()
: new Date().toISOString(),

release: event.release || '',
dist: event.dist || '',
Expand Down
150 changes: 150 additions & 0 deletions packages/core/test/profiling/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
jest.mock('../../src/js/utils/environment');
jest.mock('../../src/js/profiling/debugid');

import type { Event } from '@sentry/core';

import type { AndroidCombinedProfileEvent, CombinedProfileEvent } from '../../src/js/profiling/types';

import { getDebugMetadata } from '../../src/js/profiling/debugid';
import {
enrichAndroidProfileWithEventContext,
enrichCombinedProfileWithEventContext,
} from '../../src/js/profiling/utils';
import { getDefaultEnvironment } from '../../src/js/utils/environment';
import { createMockMinimalValidAndroidProfile, createMockMinimalValidHermesProfileEvent } from './fixtures';

describe('enrichCombinedProfileWithEventContext', () => {
beforeEach(() => {
(getDefaultEnvironment as jest.Mock).mockReturnValue('production');
(getDebugMetadata as jest.Mock).mockReturnValue([]);
});

function createMockEvent(overrides?: Partial<Event>): Event {
return {
event_id: 'test-event-id',
transaction: 'test-transaction',
release: 'test-release',
environment: 'test-env',
start_timestamp: 1000,
contexts: {
trace: {
trace_id: '12345678901234567890123456789012',
},
os: { name: 'iOS', version: '17.0' },
device: {},
},
...overrides,
};
}

test('should use profilingStartTimestampNs for timestamp when available', () => {
const profilingStartTimestampNs = 1500 * 1e9; // 1500 seconds in ns
const profile: CombinedProfileEvent = {
...createMockMinimalValidHermesProfileEvent(),
profilingStartTimestampNs,
};
const event = createMockEvent({ start_timestamp: 1000 }); // earlier than profiling start

const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result!.timestamp).toBe(new Date(profilingStartTimestampNs / 1e6).toISOString());
// Should NOT use event.start_timestamp
expect(result!.timestamp).not.toBe(new Date(1000 * 1000).toISOString());
});

test('should fall back to event.start_timestamp when profilingStartTimestampNs is not set', () => {
const profile: CombinedProfileEvent = createMockMinimalValidHermesProfileEvent();
const event = createMockEvent({ start_timestamp: 1000 });

const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result!.timestamp).toBe(new Date(1000 * 1000).toISOString());
});

test('should not include profilingStartTimestampNs in the output', () => {
const profile: CombinedProfileEvent = {
...createMockMinimalValidHermesProfileEvent(),
profilingStartTimestampNs: 1500 * 1e9,
};
const event = createMockEvent();

const result = enrichCombinedProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result).not.toHaveProperty('profilingStartTimestampNs');
});
});

describe('enrichAndroidProfileWithEventContext', () => {
beforeEach(() => {
(getDefaultEnvironment as jest.Mock).mockReturnValue('production');
(getDebugMetadata as jest.Mock).mockReturnValue([]);
});

function createMockEvent(overrides?: Partial<Event>): Event {
return {
event_id: 'test-event-id',
transaction: 'test-transaction',
release: 'test-release',
environment: 'test-env',
start_timestamp: 1000,
contexts: {
trace: {
trace_id: '12345678901234567890123456789012',
},
os: { name: 'Android', version: '14' },
device: {},
},
...overrides,
};
}

function createMockAndroidCombinedProfile(
overrides?: Partial<AndroidCombinedProfileEvent>,
): AndroidCombinedProfileEvent {
const hermesProfileEvent = createMockMinimalValidHermesProfileEvent();
return {
platform: 'android',
sampled_profile: createMockMinimalValidAndroidProfile().sampled_profile,
js_profile: hermesProfileEvent.profile,
android_api_level: 34,
duration_ns: '1000000',
active_thread_id: '123',
...overrides,
};
}

test('should use profilingStartTimestampNs for timestamp when available', () => {
const profilingStartTimestampNs = 1500 * 1e9;
const profile = createMockAndroidCombinedProfile({ profilingStartTimestampNs });
const event = createMockEvent({ start_timestamp: 1000 });

const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result!.timestamp).toBe(new Date(profilingStartTimestampNs / 1e6).toISOString());
expect(result!.timestamp).not.toBe(new Date(1000 * 1000).toISOString());
});

test('should fall back to event.start_timestamp when profilingStartTimestampNs is not set', () => {
const profile = createMockAndroidCombinedProfile();
const event = createMockEvent({ start_timestamp: 1000 });

const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result!.timestamp).toBe(new Date(1000 * 1000).toISOString());
});

test('should not include profilingStartTimestampNs in the output', () => {
const profile = createMockAndroidCombinedProfile({ profilingStartTimestampNs: 1500 * 1e9 });
const event = createMockEvent();

const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);

expect(result).not.toBeNull();
expect(result).not.toHaveProperty('profilingStartTimestampNs');
});
});
Loading