diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 9f15e81dc..79a886c3a 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -334,10 +334,14 @@ export class SegmentClient { ]); await this.onReady(); this.isReady.value = true; - // Set running to true to start event processing - await this.store.running.set(true); - // Process all pending events - await this.processPendingEvents(); + if (this.waitingPlugins.size === 0) { + // Set running to true to start event processing + await this.store.running.set(true); + } + if (this.store.running.get()) { + // Process all pending events + await this.processPendingEvents(); + } // Trigger manual flush this.flushPolicyExecuter.manualFlush(); } catch (error) { @@ -1131,15 +1135,11 @@ export class SegmentClient { * @param timeout - Milliseconds to wait before auto-resuming (default: 30000) */ pauseEventProcessing(timeout = 30000) { - // IMPORTANT: ignore repeated pauses - const running = this.store.running.get(); - if (!running) { - return; + if (this.store.running.get()) { + // Fire-and-forget: state is updated synchronously in-memory, persistence happens async + void this.store.running.set(false); } - // Fire-and-forget: state is updated synchronously in-memory, persistence happens async - void this.store.running.set(false); - // Only set timeout if not already set (prevents multiple waiting plugins from overwriting) if (!this.resumeTimeoutId) { this.resumeTimeoutId = setTimeout(async () => { @@ -1164,6 +1164,9 @@ export class SegmentClient { this.resumeTimeoutId = undefined; } await this.store.running.set(true); - await this.processPendingEvents(); + if (this.isReady.value) { + // Before init the timeline isn't configured yet, init() drains the buffer instead + await this.processPendingEvents(); + } } } diff --git a/packages/core/src/plugins/__tests__/Waiting.test.ts b/packages/core/src/plugins/__tests__/Waiting.test.ts index 31171ef34..eec43ec00 100644 --- a/packages/core/src/plugins/__tests__/Waiting.test.ts +++ b/packages/core/src/plugins/__tests__/Waiting.test.ts @@ -12,6 +12,8 @@ import { } from '../../test-helpers'; import { TrackEventType } from '../../types'; +jest.mock('../../api'); + jest.useFakeTimers(); // Type for accessing internal client properties in tests @@ -526,6 +528,65 @@ describe('WaitingPlugin', () => { expect(await client.running.get(true)).toBe(true); }); + test('WaitingPlugin added before init keeps events buffered until resume', async () => { + // Mirrors the real createClient() flow: the plugin is added before init() finishes + const preInitStore = new MockSegmentStore({ + isReady: true, + running: false, + }); + + const client = new SegmentClient({ + config: baseConfig, + logger: getMockLogger(), + store: preInitStore, + }); + + const plugin = new ManualResumeWaitingPlugin(); + client.add({ plugin }); + + client.track('Application Opened'); + await Promise.resolve(); + + expect(plugin.tracked).toBe(false); + + await client.init(); + + expect(await client.running.get(true)).toBe(false); + expect(plugin.tracked).toBe(false); + + await plugin.resume(); + + expect(await client.running.get(true)).toBe(true); + expect(plugin.tracked).toBe(true); + expect(await preInitStore.pendingEvents.get(true)).toHaveLength(0); + }); + + test('WaitingPlugin added before init still force resumes on timeout', async () => { + const preInitStore = new MockSegmentStore({ + isReady: true, + running: false, + }); + + const client = new SegmentClient({ + config: baseConfig, + logger: getMockLogger(), + store: preInitStore, + }); + + const plugin = new ManualResumeWaitingPlugin(); + client.add({ plugin }); + + client.track('Application Opened'); + await client.init(); + + expect(plugin.tracked).toBe(false); + + await jest.advanceTimersByTimeAsync(30000); + + expect(await client.running.get(true)).toBe(true); + expect(plugin.tracked).toBe(true); + }); + test('pending events queue is capped at maxPendingEvents', async () => { const client = new SegmentClient({ config: baseConfig,