Skip to content

Commit

Permalink
feat: live plugin support and enrichment closure (#1010)
Browse files Browse the repository at this point in the history
* feat: signals support and enrichment closure

* test: fix test failures

* test: add unit test for enrichment closure

* fix: fix lint issues

* feat: make enrichment closure a property of the event

* fix: lint fix

* refactor: revert changes of disabling hermes on sample app

---------

Co-authored-by: Wenxi Zeng <wzeng@twilio.com>
  • Loading branch information
wenxi-zeng and Wenxi Zeng authored Oct 8, 2024
1 parent a3a947c commit c3a6f58
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 28 deletions.
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 @@ -112,4 +112,49 @@ describe('process', () => {
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

0 comments on commit c3a6f58

Please sign in to comment.