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: add OpenFeature tracking support #995

Merged
merged 4 commits into from
Nov 19, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
"dependencies": {
"@altack/nx-bundlefy": "^0.16.0",
"@devcycle/assemblyscript-json": "^2.0.0",
"@openfeature/core": "^1.1.0",
"@openfeature/core": "^1.5.0",
"@openfeature/multi-provider": "^0.1.1",
"@openfeature/multi-provider-web": "^0.0.2",
"@openfeature/server-sdk": "^1.13.5",
"@openfeature/web-sdk": "^1.0.3",
"@openfeature/server-sdk": "^1.16.2",
"@openfeature/web-sdk": "^1.3.2",
"@swc/helpers": "~0.5.2",
"@types/express": "^4.17.17",
"@vercel/edge-config": "^1.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
OpenFeature,
Client,
StandardResolutionReasons,
ProviderEvents,
} from '@openfeature/server-sdk'
import {
DevCycleClient,
Expand All @@ -13,9 +14,12 @@ import {

jest.mock('../../src/bucketing')
jest.mock('@devcycle/config-manager')
jest.mock('../../src/eventQueue')

const variableMock = jest.spyOn(DevCycleClient.prototype, 'variable')
const cloudVariableMock = jest.spyOn(DevCycleCloudClient.prototype, 'variable')
const trackMock = jest.spyOn(DevCycleClient.prototype, 'track')
const cloudTrackMock = jest.spyOn(DevCycleCloudClient.prototype, 'track')

const logger = {
debug: jest.fn(),
Expand Down Expand Up @@ -443,5 +447,92 @@ describe.each(['DevCycleClient', 'DevCycleCloudClient'])(
})
})
})

describe(`${dvcClientType} - Tracking`, () => {
beforeEach(() => {
trackMock.mockClear()
cloudTrackMock.mockClear()
})

it('should track an event with value and metadata', async () => {
const { ofClient } = await initOFClient()
const trackingData = {
value: 123,
customField: 'custom value',
}

ofClient.track(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like it should be a test harness thing not per SDK

'test-event',
{ targetingKey: 'user-123' },
trackingData,
)

const expectedTrackCall = {
type: 'test-event',
value: 123,
metaData: {
customField: 'custom value',
},
}

if (dvcClientType === 'DevCycleClient') {
expect(trackMock).toHaveBeenCalledWith(
expect.any(DevCycleUser),
expectedTrackCall,
)
} else {
expect(cloudTrackMock).toHaveBeenCalledWith(
expect.any(DevCycleUser),
expectedTrackCall,
)
}
})

it('should track an event without value or metadata', async () => {
const { ofClient } = await initOFClient()

ofClient.track('test-event', {
targetingKey: 'user-123',
})

const expectedTrackCall = {
type: 'test-event',
}

if (dvcClientType === 'DevCycleClient') {
expect(trackMock).toHaveBeenCalledWith(
expect.any(DevCycleUser),
expectedTrackCall,
)
} else {
expect(cloudTrackMock).toHaveBeenCalledWith(
expect.any(DevCycleUser),
expectedTrackCall,
)
}
})

it('should throw error if context is missing', async () => {
const { ofClient } = await initOFClient()

ofClient.addHandler(ProviderEvents.Error, (error) => {
expect(error?.message).toBe(
'Missing targetingKey or user_id in context',
)
})
ofClient.track('test-event')
})

it('should throw error if targetingKey is missing', async () => {
const { ofClient } = await initOFClient()

ofClient.addHandler(ProviderEvents.Error, (error) => {
expect(error?.message).toBe(
'Missing targetingKey or user_id in context',
)
})
ofClient.track('test-event', {})
})
})
},
)
23 changes: 23 additions & 0 deletions sdk/nodejs/src/open-feature/DevCycleProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TargetingKeyMissingError,
InvalidContextError,
ProviderStatus,
TrackingEventDetails,
} from '@openfeature/server-sdk'
import {
DevCycleClient,
Expand Down Expand Up @@ -73,6 +74,28 @@ export class DevCycleProvider implements Provider {
await this.devcycleClient.close()
}

track(
trackingEventName: string,
context?: EvaluationContext,
trackingEventDetails?: TrackingEventDetails,
): void {
const user_id = context?.targetingKey ?? context?.user_id
if (!context || !user_id) {
throw new TargetingKeyMissingError(
'Missing targetingKey or user_id in context',
)
}

this.devcycleClient.track(this.devcycleUserFromContext(context), {
type: trackingEventName,
value: trackingEventDetails?.value,
metaData: trackingEventDetails && {
...trackingEventDetails,
value: undefined,
},
})
}

/**
* Generic function to retrieve a DVC variable and convert it to a ResolutionDetails.
* @param flagKey
Expand Down
65 changes: 65 additions & 0 deletions sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,69 @@ describe('DevCycleProvider Unit Tests', () => {
})
})
})

describe('Tracking Events', () => {
let trackMock: any
let openFeatureClient: Client
let provider: DevCycleProvider

beforeEach(async () => {
const init = await initOFClient()
openFeatureClient = init.ofClient
provider = init.provider

if (provider.devcycleClient) {
trackMock = jest
.spyOn(provider.devcycleClient, 'track')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockResolvedValue()
}
})

afterEach(() => {
trackMock?.mockClear()
})

it('should track an event with just a name', () => {
openFeatureClient.track('event-name')

expect(trackMock).toHaveBeenCalledWith({
type: 'event-name',
value: undefined,
metaData: undefined,
})
})

it('should track an event with value and metadata', () => {
openFeatureClient.track('event-name', {
value: 123,
someKey: 'someValue',
otherKey: true,
})

expect(trackMock).toHaveBeenCalledWith({
type: 'event-name',
value: 123,
metaData: {
someKey: 'someValue',
otherKey: true,
},
})
})

it('should track an event with just metadata', () => {
openFeatureClient.track('event-name', {
someKey: 'someValue',
})

expect(trackMock).toHaveBeenCalledWith({
type: 'event-name',
value: undefined,
metaData: {
someKey: 'someValue',
},
})
})
})
})
16 changes: 16 additions & 0 deletions sdk/openfeature-web-provider/src/DevCycleProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ResolutionDetails,
StandardResolutionReasons,
TargetingKeyMissingError,
TrackingEventDetails,
} from '@openfeature/web-sdk'
// Need to disable this to keep the working jest mock
// eslint-disable-next-line @nx/enforce-module-boundaries
Expand Down Expand Up @@ -116,6 +117,21 @@ export default class DevCycleProvider implements Provider {
)
}

track(
trackingEventName: string,
context?: EvaluationContext,
trackingEventDetails?: TrackingEventDetails,
): void {
this._devcycleClient?.track({
type: trackingEventName,
value: trackingEventDetails?.value,
metaData: trackingEventDetails && {
...trackingEventDetails,
value: undefined,
},
})
}

/**
* Generic function to retrieve a DVC variable and convert it to a ResolutionDetails.
* @param flagKey
Expand Down
34 changes: 17 additions & 17 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8395,10 +8395,10 @@ __metadata:
languageName: node
linkType: hard

"@openfeature/core@npm:^1.1.0":
version: 1.1.0
resolution: "@openfeature/core@npm:1.1.0"
checksum: c94dfc47b3542a837ec8378a64ab6ad2884029947e27989ec1b4ace675b5598daaa9a004f7859df26a72e208b1a880c92a05c6e90d7d9cd8e61f079ffc4f140f
"@openfeature/core@npm:^1.5.0":
version: 1.5.0
resolution: "@openfeature/core@npm:1.5.0"
checksum: b8191ef266cee855e4682b71f641edacbc36f293530ff9647f6391570874253240d8a89fe4215f7c9cbed41c9cc10c9e9de6879595553ddf2724aea3fdaff794
languageName: node
linkType: hard

Expand All @@ -8424,21 +8424,21 @@ __metadata:
languageName: node
linkType: hard

"@openfeature/server-sdk@npm:^1.13.5":
version: 1.13.5
resolution: "@openfeature/server-sdk@npm:1.13.5"
"@openfeature/server-sdk@npm:^1.16.2":
version: 1.16.2
resolution: "@openfeature/server-sdk@npm:1.16.2"
peerDependencies:
"@openfeature/core": 1.1.0
checksum: d024ebbfa3a1d63b67d78fa174663d58f0f7746792100c6d6616dfcf59cc3f48a1db3fe7027aabc9eb06f94345995710d2ebc8781fcdbbe41d81a7a4fcba2be6
"@openfeature/core": ^1.5.0
checksum: f84ab5e609887f5cec78b96c2d9f9d583d9370acf15022d5e146b3150581caba2c758f798db6d4952305bb8b7c1f87205e0f5ae6740c1910064fc3ad51c5cf43
languageName: node
linkType: hard

"@openfeature/web-sdk@npm:^1.0.3":
version: 1.0.3
resolution: "@openfeature/web-sdk@npm:1.0.3"
"@openfeature/web-sdk@npm:^1.3.2":
version: 1.3.2
resolution: "@openfeature/web-sdk@npm:1.3.2"
peerDependencies:
"@openfeature/core": 1.1.0
checksum: f84835dad77bfb7a8c762ea45dbc9362f174c37983c87436b7daca94cb230157be9f9f0a896525c5f83bd3ad63e1d4234a55bb38010352a9b1054e48fcd4f7e0
"@openfeature/core": ^1.5.0
checksum: 35fb69a071d6cca056cbedc5418d384daf5743cca9eccf0d26775e4b14bbc2aa6bc1c9e9e83458da82817f29a1db5ed66a63cd4cdb1c7599bb9bb4a4d4620d6f
languageName: node
linkType: hard

Expand Down Expand Up @@ -15316,11 +15316,11 @@ __metadata:
"@nx/web": 16.10.0
"@nx/webpack": 16.10.0
"@nx/workspace": 16.10.0
"@openfeature/core": ^1.1.0
"@openfeature/core": ^1.5.0
"@openfeature/multi-provider": ^0.1.1
"@openfeature/multi-provider-web": ^0.0.2
"@openfeature/server-sdk": ^1.13.5
"@openfeature/web-sdk": ^1.0.3
"@openfeature/server-sdk": ^1.16.2
"@openfeature/web-sdk": ^1.3.2
"@playwright/test": ^1.36.0
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
"@react-native-async-storage/async-storage": 1.19.4
Expand Down
Loading