diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f582619b..9e5cbfd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,21 +1,21 @@ name: CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] workflow_dispatch: jobs: cancel_previous: runs-on: ubuntu-latest steps: - - uses: styfle/cancel-workflow-action@0.9.1 - with: - workflow_id: ${{ github.event.workflow.id }} + - uses: styfle/cancel-workflow-action@0.9.1 + with: + workflow_id: ${{ github.event.workflow.id }} build-and-test: needs: cancel_previous runs-on: 'ubuntu-latest' - steps: + steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: @@ -30,12 +30,11 @@ jobs: run: yarn lint - name: Test run: yarn test --coverage - run-e2e-ios: needs: cancel_previous runs-on: 'macos-12' - steps: + steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 @@ -48,13 +47,16 @@ jobs: xcode-version: 14.1 - name: Install applesimutils - run: | + run: | HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew >/dev/null HOMEBREW_NO_AUTO_UPDATE=1 brew install applesimutils >/dev/null - name: Bootstrap run: yarn bootstrap + - name: Bundle Build + run: yarn build + - name: Run Server (with mocks) run: yarn example start:e2e & @@ -81,7 +83,7 @@ jobs: distribution: 'adopt' java-version: '11' cache: 'gradle' - + - name: Gradle cache uses: actions/cache@v2 with: @@ -119,6 +121,8 @@ jobs: - name: Bootstrap run: yarn install && yarn example install # No need to run bootstrap here since we don't need cocoapods + - name: Bundle build + run: yarn build - name: Run Server (with mocks) run: yarn example start:e2e & - name: Detox - Build @@ -135,6 +139,3 @@ jobs: emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true script: yarn example test:android - - - diff --git a/.gitignore b/.gitignore index 61b56129..cfbbdb04 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ coverage/ # Typescript tsconfig.tsbuildinfo + +# Library Info Auto Generated file +packages/core/src/info.ts \ No newline at end of file diff --git a/packages/core/src/__tests__/methods/process.test.ts b/packages/core/src/__tests__/methods/process.test.ts new file mode 100644 index 00000000..852977f9 --- /dev/null +++ b/packages/core/src/__tests__/methods/process.test.ts @@ -0,0 +1,117 @@ +import { SegmentClient } from '../../analytics'; +import { EventType, SegmentEvent } from '../../types'; + +import { getMockLogger } from '../__helpers__/mockLogger'; +import { MockSegmentStore } from '../__helpers__/mockSegmentStore'; + +jest.mock('uuid'); + +jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValue('2010-01-01T00:00:00.000Z'); + +describe('process', () => { + const store = new MockSegmentStore({ + userInfo: { + userId: 'current-user-id', + anonymousId: 'very-anonymous', + }, + context: { + library: { + name: 'test', + version: '1.0', + }, + }, + }); + + const clientArgs = { + config: { + writeKey: 'mock-write-key', + flushInterval: 0, + }, + logger: getMockLogger(), + store: store, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('stamps basic data: timestamp and messageId for events when not ready', async () => { + const client = new SegmentClient(clientArgs); + jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(false); + // @ts-ignore + const timeline = client.timeline; + jest.spyOn(timeline, 'process'); + + await client.track('Some Event', { id: 1 }); + + let expectedEvent: Record = { + event: 'Some Event', + properties: { + id: 1, + }, + type: EventType.TrackEvent, + }; + + // While not ready only timestamp and messageId should be defined + // @ts-ignore + const pendingEvents = client.pendingEvents; + expect(pendingEvents.length).toBe(1); + const pendingEvent = pendingEvents[0]; + expect(pendingEvent).toMatchObject(expectedEvent); + expect(pendingEvent.messageId).not.toBeUndefined(); + expect(pendingEvent.timestamp).not.toBeUndefined(); + + // Not yet processed + expect(timeline.process).not.toHaveBeenCalled(); + + // When ready it replays events + jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(true); + // @ts-ignore + await client.onReady(); + expectedEvent = { + ...expectedEvent, + context: { ...store.context.get() }, + userId: store.userInfo.get().userId, + anonymousId: store.userInfo.get().anonymousId, + }; + + // @ts-ignore + expect(client.pendingEvents.length).toBe(0); + + expect(timeline.process).toHaveBeenCalledWith( + expect.objectContaining(expectedEvent) + ); + }); + + it('stamps all context and userInfo data for events when ready', async () => { + const client = new SegmentClient(clientArgs); + jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(true); + + // @ts-ignore + const timeline = client.timeline; + jest.spyOn(timeline, 'process'); + + await client.track('Some Event', { id: 1 }); + + const expectedEvent = { + event: 'Some Event', + properties: { + id: 1, + }, + type: EventType.TrackEvent, + context: { ...store.context.get() }, + userId: store.userInfo.get().userId, + anonymousId: store.userInfo.get().anonymousId, + } as SegmentEvent; + + // @ts-ignore + const pendingEvents = client.pendingEvents; + expect(pendingEvents.length).toBe(0); + + expect(timeline.process).toHaveBeenCalledWith( + expect.objectContaining(expectedEvent) + ); + }); +}); diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 26fc6ca1..79cc6090 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -267,7 +267,7 @@ export class SegmentClient { this.trackDeepLinks(), ]); - this.onReady(); + await this.onReady(); this.isReady.value = true; // flush any stored events @@ -413,17 +413,29 @@ export class SegmentClient { } async process(incomingEvent: SegmentEvent) { - const event = await this.applyRawEventData(incomingEvent); + const event = this.applyRawEventData(incomingEvent); if (this.isReady.value) { - this.flushPolicyExecuter.notify(event); - return this.timeline.process(event); + return this.startTimelineProcessing(event); } else { this.pendingEvents.push(event); return event; } } + /** + * Starts timeline processing + * @param incomingEvent Segment Event + * @returns Segment Event + */ + private async startTimelineProcessing( + incomingEvent: SegmentEvent + ): Promise { + const event = await this.applyContextData(incomingEvent); + this.flushPolicyExecuter.notify(event); + return this.timeline.process(event); + } + private async trackDeepLinks() { if (this.getConfig().trackDeepLinks === true) { const deepLinkProperties = await this.store.deepLinkData.get(true); @@ -453,7 +465,7 @@ export class SegmentClient { * Executes when everything in the client is ready for sending events * @param isReady */ - private onReady() { + private async onReady() { // Add all plugins awaiting store if (this.pluginsToAdd.length > 0 && !this.isAddingPlugins) { this.isAddingPlugins = true; @@ -473,7 +485,7 @@ export class SegmentClient { // Send all events in the queue for (const e of this.pendingEvents) { - void this.timeline.process(e); + await this.startTimelineProcessing(e); } this.pendingEvents = []; } @@ -775,12 +787,27 @@ export class SegmentClient { } /** - * Injects context and userInfo data into the event, sets the messageId and timestamp + * Sets the messageId and timestamp + * @param event Segment Event + * @returns event with data injected + */ + private applyRawEventData = (event: SegmentEvent): SegmentEvent => { + return { + ...event, + messageId: getUUID(), + timestamp: new Date().toISOString(), + integrations: event.integrations ?? {}, + } as SegmentEvent; + }; + + /** + * Injects context and userInfo data into the event * This is handled outside of the timeline to prevent concurrency issues between plugins + * This is only added after the client is ready to let the client restore values from storage * @param event Segment Event * @returns event with data injected */ - private applyRawEventData = async ( + private applyContextData = async ( event: SegmentEvent ): Promise => { const userInfo = await this.processUserInfo(event); @@ -793,9 +820,6 @@ export class SegmentClient { ...event.context, ...context, }, - messageId: getUUID(), - timestamp: new Date().toISOString(), - integrations: event.integrations ?? {}, } as SegmentEvent; }; diff --git a/packages/core/src/info.ts b/packages/core/src/info.ts deleted file mode 100644 index 7b7239ca..00000000 --- a/packages/core/src/info.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const libraryInfo = { - name: '@segment/analytics-react-native', - version: '2.15.0', -};