Skip to content

Commit bb7375b

Browse files
committed
fix(core): Fix standalone mode and runApplication reset for appLoaded()
Fix two bugs identified in the Cursor Bugbot review: 1. Standalone mode: appLoaded() loses manual timestamp (High) When auto-capture from ReactNativeProfiler runs first in standalone mode, it sends the transaction and sets appStartDataFlushed = true. A subsequent appLoaded() call would find the flag set and silently skip re-sending. Fix: add resetAppStartDataFlushed() method on the AppStartIntegration type, called by _appLoaded() before invoking captureStandaloneAppStart(). This allows the standalone transaction to be re-sent with the correct manual timestamp. 2. isAppLoadedManuallyInvoked not reset on runApplication (Medium) The onRunApplication callback resets appStartDataFlushed and span tracking state for subsequent app starts (e.g. Android activity recreation), but did not reset isAppLoadedManuallyInvoked. After the first appLoaded() call, all subsequent app starts would have auto-capture permanently blocked. Fix: reset the flag alongside the other state in the onRunApplication callback. Adds two tests: standalone override after auto-capture, and flag reset allowing auto-capture on subsequent app starts.
1 parent 5ecc173 commit bb7375b

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

packages/core/src/js/tracing/integrations/appStart.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const INTEGRATION_NAME = 'AppStart';
3737

3838
export type AppStartIntegration = Integration & {
3939
captureStandaloneAppStart: () => Promise<void>;
40+
resetAppStartDataFlushed: () => void;
4041
};
4142

4243
/**
@@ -108,7 +109,14 @@ export async function _appLoaded(): Promise<void> {
108109
isRecordedAppStartEndTimestampMsManual = true;
109110

110111
await fetchAndUpdateEndFrames();
111-
await client.getIntegrationByName<AppStartIntegration>(INTEGRATION_NAME)?.captureStandaloneAppStart();
112+
113+
const integration = client.getIntegrationByName<AppStartIntegration>(INTEGRATION_NAME);
114+
if (integration) {
115+
// In standalone mode, auto-capture may have already flushed the transaction.
116+
// Reset the flag so captureStandaloneAppStart can re-send with the manual timestamp.
117+
integration.resetAppStartDataFlushed();
118+
await integration.captureStandaloneAppStart();
119+
}
112120
}
113121

114122
/**
@@ -294,6 +302,7 @@ export const appStartIntegration = ({
294302
appStartDataFlushed = false;
295303
firstStartedActiveRootSpanId = undefined;
296304
firstStartedActiveRootSpan = undefined;
305+
isAppLoadedManuallyInvoked = false;
297306
} else {
298307
debug.log(
299308
'[AppStartIntegration] Waiting for initial app start was flush, before updating based on runApplication call.',
@@ -604,12 +613,17 @@ export const appStartIntegration = ({
604613
);
605614
}
606615

616+
const resetAppStartDataFlushed = (): void => {
617+
appStartDataFlushed = false;
618+
};
619+
607620
return {
608621
name: INTEGRATION_NAME,
609622
setup,
610623
afterAllSetup,
611624
processEvent,
612625
captureStandaloneAppStart,
626+
resetAppStartDataFlushed,
613627
setFirstStartedActiveRootSpanId,
614628
} as AppStartIntegration;
615629
};

packages/core/test/tracing/integrations/appStart.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,96 @@ describe('appLoaded() standalone mode', () => {
12431243
expect(appStartSpan?.start_timestamp).toBeCloseTo(appStartTimeMilliseconds / 1000, 1);
12441244
expect(appStartSpan?.origin).toBe(SPAN_ORIGIN_MANUAL_APP_START);
12451245
});
1246+
1247+
it('overrides already-flushed standalone transaction when appLoaded() is called after auto-capture', async () => {
1248+
getCurrentScope().clear();
1249+
getIsolationScope().clear();
1250+
getGlobalScope().clear();
1251+
1252+
const [, appStartTimeMilliseconds] = mockAppStart({ cold: true });
1253+
1254+
const integration = appStartIntegration({ standalone: true }) as AppStartIntegrationTest;
1255+
const standaloneClient = new TestClient({
1256+
...getDefaultTestClientOptions(),
1257+
enableAppStartTracking: true,
1258+
tracesSampleRate: 1.0,
1259+
});
1260+
setCurrentClient(standaloneClient);
1261+
integration.setup(standaloneClient);
1262+
standaloneClient.addIntegration(integration);
1263+
1264+
// Simulate auto-capture from ReactNativeProfiler (componentDidMount)
1265+
const autoTimeSeconds = Date.now() / 1000;
1266+
mockFunction(timestampInSeconds).mockReturnValue(autoTimeSeconds);
1267+
await _captureAppStart({ isManual: false });
1268+
await new Promise(resolve => setTimeout(resolve, 0));
1269+
1270+
// Auto-capture should have sent the standalone transaction
1271+
expect(standaloneClient.eventQueue.length).toBe(1);
1272+
const autoEvent = standaloneClient.eventQueue[0];
1273+
const autoSpan = autoEvent?.spans?.find(s => s.op === APP_START_COLD_OP);
1274+
expect(autoSpan?.timestamp).toBeCloseTo(autoTimeSeconds, 1);
1275+
1276+
// Now call appLoaded() with a later timestamp
1277+
const manualTimeSeconds = autoTimeSeconds + 2;
1278+
mockFunction(timestampInSeconds).mockReturnValue(manualTimeSeconds);
1279+
await _appLoaded();
1280+
await new Promise(resolve => setTimeout(resolve, 0));
1281+
1282+
// appLoaded() should have sent a second standalone transaction with the manual timestamp
1283+
expect(standaloneClient.eventQueue.length).toBe(2);
1284+
const manualEvent = standaloneClient.eventQueue[1];
1285+
const manualSpan = manualEvent?.spans?.find(s => s.op === APP_START_COLD_OP);
1286+
expect(manualSpan).toBeDefined();
1287+
expect(manualSpan?.timestamp).toBeCloseTo(manualTimeSeconds, 1);
1288+
expect(manualSpan?.start_timestamp).toBeCloseTo(appStartTimeMilliseconds / 1000, 1);
1289+
expect(manualSpan?.origin).toBe(SPAN_ORIGIN_MANUAL_APP_START);
1290+
});
1291+
1292+
it('allows auto-capture again after isAppLoadedManuallyInvoked is reset', async () => {
1293+
getCurrentScope().clear();
1294+
getIsolationScope().clear();
1295+
getGlobalScope().clear();
1296+
1297+
mockAppStart({ cold: true });
1298+
1299+
const integration = appStartIntegration({ standalone: true }) as AppStartIntegrationTest;
1300+
const standaloneClient = new TestClient({
1301+
...getDefaultTestClientOptions(),
1302+
enableAppStartTracking: true,
1303+
tracesSampleRate: 1.0,
1304+
});
1305+
setCurrentClient(standaloneClient);
1306+
integration.setup(standaloneClient);
1307+
standaloneClient.addIntegration(integration);
1308+
1309+
// First app start: call appLoaded()
1310+
const firstTimeSeconds = Date.now() / 1000;
1311+
mockFunction(timestampInSeconds).mockReturnValue(firstTimeSeconds);
1312+
await _appLoaded();
1313+
await new Promise(resolve => setTimeout(resolve, 0));
1314+
expect(standaloneClient.eventQueue.length).toBe(1);
1315+
1316+
// Verify auto-capture is blocked while flag is set
1317+
const logSpy = jest.spyOn(debug, 'log');
1318+
await _captureAppStart({ isManual: false });
1319+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Skipping auto app start capture'));
1320+
1321+
// Simulate what onRunApplication does: reset flags for a new app start
1322+
_clearAppStartEndData();
1323+
mockAppStart({ cold: true });
1324+
1325+
// Now auto-capture should work again
1326+
logSpy.mockClear();
1327+
const autoTimeSeconds = Date.now() / 1000 + 1;
1328+
mockFunction(timestampInSeconds).mockReturnValue(autoTimeSeconds);
1329+
await _captureAppStart({ isManual: false });
1330+
1331+
const skippedCalls = logSpy.mock.calls.filter(
1332+
call => typeof call[0] === 'string' && call[0].includes('Skipping auto app start capture'),
1333+
);
1334+
expect(skippedCalls.length).toBe(0);
1335+
});
12461336
});
12471337

12481338
describe('Frame Data Integration', () => {

0 commit comments

Comments
 (0)