Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 15 additions & 12 deletions packages/core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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();
}
}
}
61 changes: 61 additions & 0 deletions packages/core/src/plugins/__tests__/Waiting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading