From 723eb7e80f40fc5a255e13c58a9f99650b8fc36c Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 15 Oct 2026 06:12:16 -0700 Subject: [PATCH] fix(rum): keep remote settings across app upgrades The stored remote configuration was keyed on the application version as well as the intake, application id and env, so every app upgrade discarded it. The first session after an upgrade was then drawn on the init values and reported no rcVersion, even though the previous release's settings are the better answer until the new fetch lands. Drop the application version from the identity. An installed app runs one version at a time and the console offers no per-version targeting. The version is still sent as app_version on each request. Files written by earlier builds carry the old four-part identity and no longer match, so the first launch after this SDK upgrade discards the cache once and then rewrites it under the new identity. --- README.md | 3 +- src/domain/RemoteConfiguration.spec.ts | 42 +++++++++++++++++++++++--- src/domain/RemoteConfiguration.ts | 11 ++++--- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4bc3bb15..430ae580 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,8 @@ await init({ - **Kept on disk.** The last configuration accepted is kept in the application's `userData` directory. A graceful exit retries pending or failed writes; a forced termination can leave an older cache. New sessions use it before the network answers, or without the network at all. It - applies only to the same intake, application, `env` and `version`. + applies only to the same intake, application and `env`; an application upgrade keeps it, so the + first session after one is drawn on the previous release's settings until a new answer is stored. - **Resumed sessions.** A valid session resumed from the previous launch keeps its original draw, subject to immediate activation: when what was kept asks to apply immediately, the session is judged by it at startup, as below, unless it was drawn under a newer version than the one kept — diff --git a/src/domain/RemoteConfiguration.spec.ts b/src/domain/RemoteConfiguration.spec.ts index 1d633822..ec9dd6d6 100644 --- a/src/domain/RemoteConfiguration.spec.ts +++ b/src/domain/RemoteConfiguration.spec.ts @@ -65,7 +65,7 @@ function storedFile(overrides: Record = {}) { return JSON.stringify({ format: 1, sdkVersion: 'test', - identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod', '1.2.3']), + identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod']), version: 2, values: { sessionSampleRate: 0, sessionOnError: true }, custom: { flag: 'cached' }, @@ -473,7 +473,7 @@ describe('RemoteConfiguration', () => { { format: 1, sdkVersion: 'test', - identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod', '1.2.3']), + identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod']), version: 3, values: { sessionSampleRate: 25, sessionOnError: true }, custom: { flag: 1 }, @@ -497,9 +497,13 @@ describe('RemoteConfiguration', () => { }); it.each([ - ['another env', { identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'staging', '1.2.3']) }], - ['another app version', { identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod', '1.2.2']) }], - ['another application', { identity: JSON.stringify(['http://proxy.test', 'other-app', 'prod', '1.2.3']) }], + ['another env', { identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'staging']) }], + ['another intake', { identity: JSON.stringify(['http://other-proxy.test', 'test-app-id', 'prod']) }], + [ + 'a build that keyed it on the app version', + { identity: JSON.stringify(['http://proxy.test', 'test-app-id', 'prod', '1.2.3']) }, + ], + ['another application', { identity: JSON.stringify(['http://proxy.test', 'other-app', 'prod']) }], ['another file format', { format: 2 }], ['a rate out of range', { values: { sessionSampleRate: 200 } }], ['a switch that is not a boolean', { values: { sessionOnError: 'true' } }], @@ -516,6 +520,34 @@ describe('RemoteConfiguration', () => { expect(requestHeaders()).toEqual({}); }); + it('applies a file written under another app version', async () => { + config = { ...config, version: '2.0.0' }; + mfs.readFile.mockResolvedValue(storedFile()); + fetchMock.mockReturnValue(new Promise(() => undefined)); + + const configuration = await start(); + + expect(configuration.getSampling()).toEqual({ sessionSampleRate: 0, sessionOnError: true, rcVersion: 2 }); + expect(requestedUrl().searchParams.get('app_version')).toBe('2.0.0'); + expect(requestedUrl().searchParams.get('applied_version')).toBe('2'); + }); + + it('writes the same identity whatever the app version', async () => { + fetchMock.mockImplementation(() => Promise.resolve(ok(configurationBody()))); + const identityWritten = async (version: string | undefined) => { + writtenFiles().length = 0; + mfs.writeFile.mockClear(); + config = { ...config, version }; + remote?.stop(); + await start(); + return (writtenFiles()[0] as { identity: string }).identity; + }; + + const first = await identityWritten('1.2.3'); + expect(await identityWritten('2.0.0')).toBe(first); + expect(await identityWritten(undefined)).toBe(first); + }); + it('reads a kept version 0 as no configuration, whatever it holds', async () => { mfs.readFile.mockResolvedValue(storedFile({ version: 0, values: { sessionSampleRate: 0 } })); fetchMock.mockReturnValue(new Promise(() => undefined)); diff --git a/src/domain/RemoteConfiguration.ts b/src/domain/RemoteConfiguration.ts index 27424d3f..fc116853 100644 --- a/src/domain/RemoteConfiguration.ts +++ b/src/domain/RemoteConfiguration.ts @@ -105,7 +105,7 @@ interface StoredConfiguration extends Omit