Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: live plugin support and enrichment closure #1010

Merged
merged 7 commits into from
Oct 8, 2024
Merged
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
6 changes: 3 additions & 3 deletions examples/AnalyticsReactNativeExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ PODS:
- RNScreens (3.27.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- segment-analytics-react-native (2.19.4):
- segment-analytics-react-native (2.19.5):
- React-Core
- sovran-react-native
- SocketRocket (0.6.1)
Expand Down Expand Up @@ -752,12 +752,12 @@ SPEC CHECKSUMS:
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741
RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581
segment-analytics-react-native: 49ce29a68e86b38c084f1ce07b0c128273d169f9
segment-analytics-react-native: 4bac3da03dd4a1eed178786b1d7025cd2c0ed6c9
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
sovran-react-native: 5f02bd2d111ffe226d00c7b0435290eae6f10934
Yoga: eddf2bbe4a896454c248a8f23b4355891eb720a6
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: 329f06ebb76294acf15c298d0af45530e2797740

COCOAPODS: 1.11.3
COCOAPODS: 1.15.2
28 changes: 24 additions & 4 deletions packages/core/src/__tests__/internal/fetchSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ describe('internal #getSettings', () => {
await client.fetchSettings();

expect(fetch).toHaveBeenCalledWith(
`${settingsCDN}/${clientArgs.config.writeKey}/settings`
`${settingsCDN}/${clientArgs.config.writeKey}/settings`,
{
headers: {
'Cache-Control': 'no-cache',
},
}
);

expect(setSettingsSpy).toHaveBeenCalledWith(mockJSONResponse.integrations);
Expand All @@ -66,7 +71,12 @@ describe('internal #getSettings', () => {
await client.fetchSettings();

expect(fetch).toHaveBeenCalledWith(
`${settingsCDN}/${clientArgs.config.writeKey}/settings`
`${settingsCDN}/${clientArgs.config.writeKey}/settings`,
{
headers: {
'Cache-Control': 'no-cache',
},
}
);

expect(setSettingsSpy).toHaveBeenCalledWith(
Expand All @@ -92,7 +102,12 @@ describe('internal #getSettings', () => {
await anotherClient.fetchSettings();

expect(fetch).toHaveBeenCalledWith(
`${settingsCDN}/${clientArgs.config.writeKey}/settings`
`${settingsCDN}/${clientArgs.config.writeKey}/settings`,
{
headers: {
'Cache-Control': 'no-cache',
},
}
);
expect(setSettingsSpy).not.toHaveBeenCalled();
});
Expand All @@ -113,7 +128,12 @@ describe('internal #getSettings', () => {
await anotherClient.fetchSettings();

expect(fetch).toHaveBeenCalledWith(
`${settingsCDN}/${clientArgs.config.writeKey}/settings`
`${settingsCDN}/${clientArgs.config.writeKey}/settings`,
{
headers: {
'Cache-Control': 'no-cache',
},
}
);
expect(setSettingsSpy).not.toHaveBeenCalled();
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/methods/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ describe('methods #group', () => {
};

expect(client.process).toHaveBeenCalledTimes(1);
expect(client.process).toHaveBeenCalledWith(expectedEvent);
expect(client.process).toHaveBeenCalledWith(expectedEvent, undefined);
});
});
45 changes: 45 additions & 0 deletions packages/core/src/__tests__/methods/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
it('stamps basic data: timestamp and messageId for pending events when not ready', async () => {
const client = new SegmentClient(clientArgs);
jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(false);
// @ts-ignore

Check warning on line 41 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const timeline = client.timeline;
jest.spyOn(timeline, 'process');

Expand All @@ -53,7 +53,7 @@
};

// While not ready only timestamp and messageId should be defined
// @ts-ignore

Check warning on line 56 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const pendingEvents = client.store.pendingEvents.get();
expect(pendingEvents.length).toBe(1);
const pendingEvent = pendingEvents[0];
Expand All @@ -66,7 +66,7 @@

// When ready it replays events
jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(true);
// @ts-ignore

Check warning on line 69 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
await client.onReady();
expectedEvent = {
...expectedEvent,
Expand All @@ -75,7 +75,7 @@
anonymousId: store.userInfo.get().anonymousId,
};

// @ts-ignore

Check warning on line 78 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
expect(client.store.pendingEvents.get().length).toBe(0);

expect(timeline.process).toHaveBeenCalledWith(
Expand All @@ -87,7 +87,7 @@
const client = new SegmentClient(clientArgs);
jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(true);

// @ts-ignore

Check warning on line 90 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const timeline = client.timeline;
jest.spyOn(timeline, 'process');

Expand All @@ -104,7 +104,7 @@
anonymousId: store.userInfo.get().anonymousId,
} as SegmentEvent;

// @ts-ignore

Check warning on line 107 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const pendingEvents = client.store.pendingEvents.get();
expect(pendingEvents.length).toBe(0);

Expand All @@ -112,4 +112,49 @@
expect.objectContaining(expectedEvent)
);
});

it('enrichment closure gets applied', async () => {
const client = new SegmentClient(clientArgs);
jest.spyOn(client.isReady, 'value', 'get').mockReturnValue(true);

// @ts-ignore

Check warning on line 120 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const timeline = client.timeline;
jest.spyOn(timeline, 'process');

await client.track('Some Event', { id: 1 }, (event) => {
if (event.context == null) {
event.context = {};
}
event.context.__eventOrigin = {
type: 'signals',
};
event.anonymousId = 'foo';

return event;
});

const expectedEvent = {
event: 'Some Event',
properties: {
id: 1,
},
type: EventType.TrackEvent,
context: {
__eventOrigin: {
type: 'signals',
},
...store.context.get(),
},
userId: store.userInfo.get().userId,
anonymousId: 'foo',
} as SegmentEvent;

// @ts-ignore

Check warning on line 152 in packages/core/src/__tests__/methods/process.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Do not use "@ts-ignore" because it alters compilation errors
const pendingEvents = client.store.pendingEvents.get();
expect(pendingEvents.length).toBe(0);

expect(timeline.process).toHaveBeenCalledWith(
expect.objectContaining(expectedEvent)
);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/methods/screen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ describe('methods #screen', () => {
};

expect(client.process).toHaveBeenCalledTimes(1);
expect(client.process).toHaveBeenCalledWith(expectedEvent);
expect(client.process).toHaveBeenCalledWith(expectedEvent, undefined);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/methods/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ describe('methods #track', () => {
};

expect(client.process).toHaveBeenCalledTimes(1);
expect(client.process).toHaveBeenCalledWith(expectedEvent);
expect(client.process).toHaveBeenCalledWith(expectedEvent, undefined);
});
});
73 changes: 58 additions & 15 deletions packages/core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ import {
Watchable,
} from './storage';
import { Timeline } from './timeline';
import { DestinationFilters, EventType, SegmentAPISettings } from './types';
import {
DestinationFilters,
EventType,
SegmentAPISettings,
SegmentAPIConsentSettings,
EdgeFunctionSettings,
EnrichmentClosure,
} from './types';
import {
Config,
Context,
Expand All @@ -59,7 +66,6 @@ import {
SegmentError,
translateHTTPError,
} from './errors';
import type { SegmentAPIConsentSettings } from '.';

type OnPluginAddedCallback = (plugin: Plugin) => void;

Expand Down Expand Up @@ -125,6 +131,11 @@ export class SegmentClient {
*/
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined>;

/**
* Access or subscribe to edge functions settings
*/
readonly edgeFunctionSettings: Watchable<EdgeFunctionSettings | undefined>;

/**
* Access or subscribe to destination filter settings
*/
Expand Down Expand Up @@ -212,6 +223,11 @@ export class SegmentClient {
onChange: this.store.consentSettings.onChange,
};

this.edgeFunctionSettings = {
get: this.store.edgeFunctionSettings.get,
onChange: this.store.edgeFunctionSettings.onChange,
};

this.filters = {
get: this.store.filters.get,
onChange: this.store.filters.onChange,
Expand Down Expand Up @@ -307,20 +323,26 @@ export class SegmentClient {
const settingsEndpoint = `${settingsPrefix}/${this.config.writeKey}/settings`;

try {
const res = await fetch(settingsEndpoint);
const res = await fetch(settingsEndpoint, {
headers: {
'Cache-Control': 'no-cache',
},
});
checkResponseForErrors(res);

const resJson: SegmentAPISettings =
(await res.json()) as SegmentAPISettings;
const integrations = resJson.integrations;
const consentSettings = resJson.consentSettings;
const edgeFunctionSettings = resJson.edgeFunction;
const filters = this.generateFiltersMap(
resJson.middlewareSettings?.routingRules ?? []
);
this.logger.info('Received settings from Segment succesfully.');
await Promise.all([
this.store.settings.set(integrations),
this.store.consentSettings.set(consentSettings),
this.store.edgeFunctionSettings.set(edgeFunctionSettings),
this.store.filters.set(filters),
]);
} catch (e) {
Expand Down Expand Up @@ -422,8 +444,9 @@ export class SegmentClient {
this.timeline.remove(plugin);
}

async process(incomingEvent: SegmentEvent) {
async process(incomingEvent: SegmentEvent, enrichment?: EnrichmentClosure) {
const event = this.applyRawEventData(incomingEvent);
event.enrichment = enrichment;

if (this.isReady.value) {
return this.startTimelineProcessing(event);
Expand Down Expand Up @@ -536,47 +559,63 @@ export class SegmentClient {
}
}

async screen(name: string, options?: JsonMap) {
async screen(
name: string,
options?: JsonMap,
enrichment?: EnrichmentClosure
) {
const event = createScreenEvent({
name,
properties: options,
});

await this.process(event);
await this.process(event, enrichment);
this.logger.info('SCREEN event saved', event);
}

async track(eventName: string, options?: JsonMap) {
async track(
eventName: string,
options?: JsonMap,
enrichment?: EnrichmentClosure
) {
const event = createTrackEvent({
event: eventName,
properties: options,
});

await this.process(event);
await this.process(event, enrichment);
this.logger.info('TRACK event saved', event);
}

async identify(userId?: string, userTraits?: UserTraits) {
async identify(
userId?: string,
userTraits?: UserTraits,
enrichment?: EnrichmentClosure
) {
const event = createIdentifyEvent({
userId: userId,
userTraits: userTraits,
});

await this.process(event);
await this.process(event, enrichment);
this.logger.info('IDENTIFY event saved', event);
}

async group(groupId: string, groupTraits?: GroupTraits) {
async group(
groupId: string,
groupTraits?: GroupTraits,
enrichment?: EnrichmentClosure
) {
const event = createGroupEvent({
groupId,
groupTraits,
});

await this.process(event);
await this.process(event, enrichment);
this.logger.info('GROUP event saved', event);
}

async alias(newUserId: string) {
async alias(newUserId: string, enrichment?: EnrichmentClosure) {
// We don't use a concurrency safe version of get here as we don't want to lock the values yet,
// we will update the values correctly when InjectUserInfo processes the change
const { anonymousId, userId: previousUserId } = this.store.userInfo.get();
Expand All @@ -587,7 +626,7 @@ export class SegmentClient {
newUserId,
});

await this.process(event);
await this.process(event, enrichment);
this.logger.info('ALIAS event saved', event);
}

Expand Down Expand Up @@ -721,7 +760,11 @@ export class SegmentClient {
* @param callback Function to call
*/
onPluginLoaded(callback: OnPluginAddedCallback) {
this.onPluginAddedObservers.push(callback);
const i = this.onPluginAddedObservers.push(callback);

return () => {
this.onPluginAddedObservers.splice(i, 1);
};
}

private triggerOnPluginLoaded(plugin: Plugin) {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { defaultConfig } from './constants';
export * from './client';
export * from './plugin';
export * from './types';
Expand All @@ -12,8 +13,12 @@ export {
objectToString,
unknownToString,
deepCompare,
chunk,
} from './util';
export { SegmentClient } from './analytics';
export { QueueFlushingPlugin } from './plugins/QueueFlushingPlugin';
export { createTrackEvent } from './events';
export { uploadEvents } from './api';
export { SegmentDestination } from './plugins/SegmentDestination';
export {
type CategoryConsentStatusProvider,
Expand Down
Loading
Loading