Skip to content

Commit b05d7be

Browse files
feat: add OpenFeature tracking support (#995)
1 parent 69ccc91 commit b05d7be

File tree

6 files changed

+215
-20
lines changed

6 files changed

+215
-20
lines changed

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
"dependencies": {
3434
"@altack/nx-bundlefy": "^0.16.0",
3535
"@devcycle/assemblyscript-json": "^2.0.0",
36-
"@openfeature/core": "^1.1.0",
36+
"@openfeature/core": "^1.5.0",
3737
"@openfeature/multi-provider": "^0.1.1",
3838
"@openfeature/multi-provider-web": "^0.0.2",
39-
"@openfeature/server-sdk": "^1.13.5",
40-
"@openfeature/web-sdk": "^1.0.3",
39+
"@openfeature/server-sdk": "^1.16.2",
40+
"@openfeature/web-sdk": "^1.3.2",
4141
"@swc/helpers": "~0.5.2",
4242
"@types/express": "^4.17.17",
4343
"@vercel/edge-config": "^1.2.0",

sdk/nodejs/__tests__/open-feature-provider/DevCycleProvider.test.ts

+91
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
OpenFeature,
33
Client,
44
StandardResolutionReasons,
5+
ProviderEvents,
56
} from '@openfeature/server-sdk'
67
import {
78
DevCycleClient,
@@ -13,9 +14,12 @@ import {
1314

1415
jest.mock('../../src/bucketing')
1516
jest.mock('@devcycle/config-manager')
17+
jest.mock('../../src/eventQueue')
1618

1719
const variableMock = jest.spyOn(DevCycleClient.prototype, 'variable')
1820
const cloudVariableMock = jest.spyOn(DevCycleCloudClient.prototype, 'variable')
21+
const trackMock = jest.spyOn(DevCycleClient.prototype, 'track')
22+
const cloudTrackMock = jest.spyOn(DevCycleCloudClient.prototype, 'track')
1923

2024
const logger = {
2125
debug: jest.fn(),
@@ -443,5 +447,92 @@ describe.each(['DevCycleClient', 'DevCycleCloudClient'])(
443447
})
444448
})
445449
})
450+
451+
describe(`${dvcClientType} - Tracking`, () => {
452+
beforeEach(() => {
453+
trackMock.mockClear()
454+
cloudTrackMock.mockClear()
455+
})
456+
457+
it('should track an event with value and metadata', async () => {
458+
const { ofClient } = await initOFClient()
459+
const trackingData = {
460+
value: 123,
461+
customField: 'custom value',
462+
}
463+
464+
ofClient.track(
465+
'test-event',
466+
{ targetingKey: 'user-123' },
467+
trackingData,
468+
)
469+
470+
const expectedTrackCall = {
471+
type: 'test-event',
472+
value: 123,
473+
metaData: {
474+
customField: 'custom value',
475+
},
476+
}
477+
478+
if (dvcClientType === 'DevCycleClient') {
479+
expect(trackMock).toHaveBeenCalledWith(
480+
expect.any(DevCycleUser),
481+
expectedTrackCall,
482+
)
483+
} else {
484+
expect(cloudTrackMock).toHaveBeenCalledWith(
485+
expect.any(DevCycleUser),
486+
expectedTrackCall,
487+
)
488+
}
489+
})
490+
491+
it('should track an event without value or metadata', async () => {
492+
const { ofClient } = await initOFClient()
493+
494+
ofClient.track('test-event', {
495+
targetingKey: 'user-123',
496+
})
497+
498+
const expectedTrackCall = {
499+
type: 'test-event',
500+
}
501+
502+
if (dvcClientType === 'DevCycleClient') {
503+
expect(trackMock).toHaveBeenCalledWith(
504+
expect.any(DevCycleUser),
505+
expectedTrackCall,
506+
)
507+
} else {
508+
expect(cloudTrackMock).toHaveBeenCalledWith(
509+
expect.any(DevCycleUser),
510+
expectedTrackCall,
511+
)
512+
}
513+
})
514+
515+
it('should throw error if context is missing', async () => {
516+
const { ofClient } = await initOFClient()
517+
518+
ofClient.addHandler(ProviderEvents.Error, (error) => {
519+
expect(error?.message).toBe(
520+
'Missing targetingKey or user_id in context',
521+
)
522+
})
523+
ofClient.track('test-event')
524+
})
525+
526+
it('should throw error if targetingKey is missing', async () => {
527+
const { ofClient } = await initOFClient()
528+
529+
ofClient.addHandler(ProviderEvents.Error, (error) => {
530+
expect(error?.message).toBe(
531+
'Missing targetingKey or user_id in context',
532+
)
533+
})
534+
ofClient.track('test-event', {})
535+
})
536+
})
446537
},
447538
)

sdk/nodejs/src/open-feature/DevCycleProvider.ts

+23
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
TargetingKeyMissingError,
1111
InvalidContextError,
1212
ProviderStatus,
13+
TrackingEventDetails,
1314
} from '@openfeature/server-sdk'
1415
import {
1516
DevCycleClient,
@@ -73,6 +74,28 @@ export class DevCycleProvider implements Provider {
7374
await this.devcycleClient.close()
7475
}
7576

77+
track(
78+
trackingEventName: string,
79+
context?: EvaluationContext,
80+
trackingEventDetails?: TrackingEventDetails,
81+
): void {
82+
const user_id = context?.targetingKey ?? context?.user_id
83+
if (!context || !user_id) {
84+
throw new TargetingKeyMissingError(
85+
'Missing targetingKey or user_id in context',
86+
)
87+
}
88+
89+
this.devcycleClient.track(this.devcycleUserFromContext(context), {
90+
type: trackingEventName,
91+
value: trackingEventDetails?.value,
92+
metaData: trackingEventDetails && {
93+
...trackingEventDetails,
94+
value: undefined,
95+
},
96+
})
97+
}
98+
7699
/**
77100
* Generic function to retrieve a DVC variable and convert it to a ResolutionDetails.
78101
* @param flagKey

sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts

+65
Original file line numberDiff line numberDiff line change
@@ -438,4 +438,69 @@ describe('DevCycleProvider Unit Tests', () => {
438438
})
439439
})
440440
})
441+
442+
describe('Tracking Events', () => {
443+
let trackMock: any
444+
let openFeatureClient: Client
445+
let provider: DevCycleProvider
446+
447+
beforeEach(async () => {
448+
const init = await initOFClient()
449+
openFeatureClient = init.ofClient
450+
provider = init.provider
451+
452+
if (provider.devcycleClient) {
453+
trackMock = jest
454+
.spyOn(provider.devcycleClient, 'track')
455+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
456+
// @ts-ignore
457+
.mockResolvedValue()
458+
}
459+
})
460+
461+
afterEach(() => {
462+
trackMock?.mockClear()
463+
})
464+
465+
it('should track an event with just a name', () => {
466+
openFeatureClient.track('event-name')
467+
468+
expect(trackMock).toHaveBeenCalledWith({
469+
type: 'event-name',
470+
value: undefined,
471+
metaData: undefined,
472+
})
473+
})
474+
475+
it('should track an event with value and metadata', () => {
476+
openFeatureClient.track('event-name', {
477+
value: 123,
478+
someKey: 'someValue',
479+
otherKey: true,
480+
})
481+
482+
expect(trackMock).toHaveBeenCalledWith({
483+
type: 'event-name',
484+
value: 123,
485+
metaData: {
486+
someKey: 'someValue',
487+
otherKey: true,
488+
},
489+
})
490+
})
491+
492+
it('should track an event with just metadata', () => {
493+
openFeatureClient.track('event-name', {
494+
someKey: 'someValue',
495+
})
496+
497+
expect(trackMock).toHaveBeenCalledWith({
498+
type: 'event-name',
499+
value: undefined,
500+
metaData: {
501+
someKey: 'someValue',
502+
},
503+
})
504+
})
505+
})
441506
})

sdk/openfeature-web-provider/src/DevCycleProvider.ts

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ResolutionDetails,
1313
StandardResolutionReasons,
1414
TargetingKeyMissingError,
15+
TrackingEventDetails,
1516
} from '@openfeature/web-sdk'
1617
// Need to disable this to keep the working jest mock
1718
// eslint-disable-next-line @nx/enforce-module-boundaries
@@ -116,6 +117,21 @@ export default class DevCycleProvider implements Provider {
116117
)
117118
}
118119

120+
track(
121+
trackingEventName: string,
122+
context?: EvaluationContext,
123+
trackingEventDetails?: TrackingEventDetails,
124+
): void {
125+
this._devcycleClient?.track({
126+
type: trackingEventName,
127+
value: trackingEventDetails?.value,
128+
metaData: trackingEventDetails && {
129+
...trackingEventDetails,
130+
value: undefined,
131+
},
132+
})
133+
}
134+
119135
/**
120136
* Generic function to retrieve a DVC variable and convert it to a ResolutionDetails.
121137
* @param flagKey

yarn.lock

+17-17
Original file line numberDiff line numberDiff line change
@@ -8395,10 +8395,10 @@ __metadata:
83958395
languageName: node
83968396
linkType: hard
83978397

8398-
"@openfeature/core@npm:^1.1.0":
8399-
version: 1.1.0
8400-
resolution: "@openfeature/core@npm:1.1.0"
8401-
checksum: c94dfc47b3542a837ec8378a64ab6ad2884029947e27989ec1b4ace675b5598daaa9a004f7859df26a72e208b1a880c92a05c6e90d7d9cd8e61f079ffc4f140f
8398+
"@openfeature/core@npm:^1.5.0":
8399+
version: 1.5.0
8400+
resolution: "@openfeature/core@npm:1.5.0"
8401+
checksum: b8191ef266cee855e4682b71f641edacbc36f293530ff9647f6391570874253240d8a89fe4215f7c9cbed41c9cc10c9e9de6879595553ddf2724aea3fdaff794
84028402
languageName: node
84038403
linkType: hard
84048404

@@ -8424,21 +8424,21 @@ __metadata:
84248424
languageName: node
84258425
linkType: hard
84268426

8427-
"@openfeature/server-sdk@npm:^1.13.5":
8428-
version: 1.13.5
8429-
resolution: "@openfeature/server-sdk@npm:1.13.5"
8427+
"@openfeature/server-sdk@npm:^1.16.2":
8428+
version: 1.16.2
8429+
resolution: "@openfeature/server-sdk@npm:1.16.2"
84308430
peerDependencies:
8431-
"@openfeature/core": 1.1.0
8432-
checksum: d024ebbfa3a1d63b67d78fa174663d58f0f7746792100c6d6616dfcf59cc3f48a1db3fe7027aabc9eb06f94345995710d2ebc8781fcdbbe41d81a7a4fcba2be6
8431+
"@openfeature/core": ^1.5.0
8432+
checksum: f84ab5e609887f5cec78b96c2d9f9d583d9370acf15022d5e146b3150581caba2c758f798db6d4952305bb8b7c1f87205e0f5ae6740c1910064fc3ad51c5cf43
84338433
languageName: node
84348434
linkType: hard
84358435

8436-
"@openfeature/web-sdk@npm:^1.0.3":
8437-
version: 1.0.3
8438-
resolution: "@openfeature/web-sdk@npm:1.0.3"
8436+
"@openfeature/web-sdk@npm:^1.3.2":
8437+
version: 1.3.2
8438+
resolution: "@openfeature/web-sdk@npm:1.3.2"
84398439
peerDependencies:
8440-
"@openfeature/core": 1.1.0
8441-
checksum: f84835dad77bfb7a8c762ea45dbc9362f174c37983c87436b7daca94cb230157be9f9f0a896525c5f83bd3ad63e1d4234a55bb38010352a9b1054e48fcd4f7e0
8440+
"@openfeature/core": ^1.5.0
8441+
checksum: 35fb69a071d6cca056cbedc5418d384daf5743cca9eccf0d26775e4b14bbc2aa6bc1c9e9e83458da82817f29a1db5ed66a63cd4cdb1c7599bb9bb4a4d4620d6f
84428442
languageName: node
84438443
linkType: hard
84448444

@@ -15316,11 +15316,11 @@ __metadata:
1531615316
"@nx/web": 16.10.0
1531715317
"@nx/webpack": 16.10.0
1531815318
"@nx/workspace": 16.10.0
15319-
"@openfeature/core": ^1.1.0
15319+
"@openfeature/core": ^1.5.0
1532015320
"@openfeature/multi-provider": ^0.1.1
1532115321
"@openfeature/multi-provider-web": ^0.0.2
15322-
"@openfeature/server-sdk": ^1.13.5
15323-
"@openfeature/web-sdk": ^1.0.3
15322+
"@openfeature/server-sdk": ^1.16.2
15323+
"@openfeature/web-sdk": ^1.3.2
1532415324
"@playwright/test": ^1.36.0
1532515325
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
1532615326
"@react-native-async-storage/async-storage": 1.19.4

0 commit comments

Comments
 (0)