Skip to content

Commit 9a15354

Browse files
committed
wip
1 parent 01fcb93 commit 9a15354

File tree

19 files changed

+156
-35
lines changed

19 files changed

+156
-35
lines changed

packages/server/src/client/client.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import type {
88
import type { Features } from '../evaluation';
99
import type { ProviderStatus } from '../provider';
1010
import type { ProviderEvents } from '../events';
11+
import type { Tracking } from '../tracking';
1112

1213
export interface Client
1314
extends EvaluationLifeCycle<Client>,
1415
Features,
1516
ManageContext<Client>,
1617
ManageLogger<Client>,
18+
Tracking,
1719
Eventing<ProviderEvents> {
1820
readonly metadata: ClientMetadata;
1921
/**

packages/server/src/client/internal/open-feature-client.ts

+35-13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
HookContext,
99
JsonValue,
1010
Logger,
11+
TrackingEventDetails,
1112
OpenFeatureError,
1213
ResolutionDetails} from '@openfeature/core';
1314
import {
@@ -223,6 +224,19 @@ export class OpenFeatureClient implements Client {
223224
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', context, options);
224225
}
225226

227+
track(occurrenceKey: string, context: EvaluationContext, occurrenceDetails: TrackingEventDetails): void {
228+
229+
this.shortCircuitIfNotReady();
230+
231+
const mergedContext = this.mergeContexts(context);
232+
233+
if (typeof this._provider.track === 'function') {
234+
return this._provider.track?.(occurrenceKey, mergedContext, occurrenceDetails);
235+
} else {
236+
this._logger.debug('Provider does not implement track function: will no-op.');
237+
}
238+
}
239+
226240
private async evaluate<T extends FlagValue>(
227241
flagKey: string,
228242
resolver: (
@@ -246,13 +260,7 @@ export class OpenFeatureClient implements Client {
246260
];
247261
const allHooksReversed = [...allHooks].reverse();
248262

249-
// merge global and client contexts
250-
const mergedContext = {
251-
...OpenFeature.getContext(),
252-
...OpenFeature.getTransactionContext(),
253-
...this._context,
254-
...invocationContext,
255-
};
263+
const mergedContext = this.mergeContexts(invocationContext);
256264

257265
// this reference cannot change during the course of evaluation
258266
// it may be used as a key in WeakMaps
@@ -269,12 +277,7 @@ export class OpenFeatureClient implements Client {
269277
try {
270278
const frozenContext = await this.beforeHooks(allHooks, hookContext, options);
271279

272-
// short circuit evaluation entirely if provider is in a bad state
273-
if (this.providerStatus === ProviderStatus.NOT_READY) {
274-
throw new ProviderNotReadyError('provider has not yet initialized');
275-
} else if (this.providerStatus === ProviderStatus.FATAL) {
276-
throw new ProviderFatalError('provider is in an irrecoverable error state');
277-
}
280+
this.shortCircuitIfNotReady();
278281

279282
// run the referenced resolver, binding the provider.
280283
const resolution = await resolver.call(this._provider, flagKey, defaultValue, frozenContext, this._logger);
@@ -380,4 +383,23 @@ export class OpenFeatureClient implements Client {
380383
private get _logger() {
381384
return this._clientLogger || this.globalLogger();
382385
}
386+
387+
private mergeContexts(invocationContext: EvaluationContext) {
388+
// merge global and client contexts
389+
return {
390+
...OpenFeature.getContext(),
391+
...OpenFeature.getTransactionContext(),
392+
...this._context,
393+
...invocationContext,
394+
};
395+
}
396+
397+
private shortCircuitIfNotReady() {
398+
// short circuit evaluation entirely if provider is in a bad state
399+
if (this.providerStatus === ProviderStatus.NOT_READY) {
400+
throw new ProviderNotReadyError('provider has not yet initialized');
401+
} else if (this.providerStatus === ProviderStatus.FATAL) {
402+
throw new ProviderFatalError('provider is in an irrecoverable error state');
403+
}
404+
}
383405
}

packages/server/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from './open-feature';
55
export * from './transaction-context';
66
export * from './events';
77
export * from './hooks';
8+
export * from './tracking';
89
export * from '@openfeature/core';

packages/server/src/provider/provider.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import type { CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails} from '@openfeature/core';
2-
import { ServerProviderStatus } from '@openfeature/core';
1+
import type {
2+
CommonProvider,
3+
EvaluationContext,
4+
JsonValue,
5+
Logger,
6+
TrackingEventDetails,
7+
ResolutionDetails} from '@openfeature/core';
8+
import {
9+
ServerProviderStatus,
10+
} from '@openfeature/core';
311
import type { Hook } from '../hooks';
412

513
export { ServerProviderStatus as ProviderStatus };
@@ -58,4 +66,12 @@ export interface Provider extends CommonProvider<ServerProviderStatus> {
5866
context: EvaluationContext,
5967
logger: Logger,
6068
): Promise<ResolutionDetails<T>>;
69+
70+
/**
71+
* Track a user action or application state, usually representing a business objective or outcome.
72+
* @param trackingEventName
73+
* @param context
74+
* @param trackingEventDetails
75+
*/
76+
track?(trackingEventName: string, context?: EvaluationContext, trackingEventDetails?: TrackingEventDetails): void;
6177
}

packages/server/src/tracking/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tracking';
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { EvaluationContext, TrackingEventDetails } from '@openfeature/core';
2+
3+
export interface Tracking {
4+
5+
/**
6+
* Track a user action or application state, usually representing a business objective or outcome.
7+
* @param trackingEventName an identifier for the event
8+
* @param context the evaluation context
9+
* @param trackingEventDetails the details of the tracking event
10+
*/
11+
track(trackingEventName: string, context?: EvaluationContext, trackingEventDetails?: TrackingEventDetails): void;
12+
}

packages/shared/src/evaluation/context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PrimitiveValue } from './evaluation';
1+
import type { PrimitiveValue } from '../types';
22

33
export type EvaluationContextValue =
44
| PrimitiveValue

packages/shared/src/evaluation/evaluation.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
export type FlagValueType = 'boolean' | 'string' | 'number' | 'object';
2-
3-
export type PrimitiveValue = null | boolean | string | number;
4-
export type JsonObject = { [key: string]: JsonValue };
5-
export type JsonArray = JsonValue[];
1+
import type { JsonValue } from '../types/structure';
62

7-
/**
8-
* Represents a JSON node value.
9-
*/
10-
export type JsonValue = PrimitiveValue | JsonObject | JsonArray;
3+
export type FlagValueType = 'boolean' | 'string' | 'number' | 'object';
114

125
/**
136
* Represents a JSON node value, or Date.

packages/shared/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export * from './logger';
77
export * from './provider';
88
export * from './evaluation';
99
export * from './type-guards';
10+
export * from './tracking';
1011
export * from './open-feature';

packages/shared/src/tracking/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tracking-event';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { PrimitiveValue } from '../types';
2+
3+
export type TrackingEventValue =
4+
| PrimitiveValue
5+
| Date
6+
| { [key: string]: TrackingEventValue }
7+
| TrackingEventValue[];
8+
9+
/**
10+
* A container for arbitrary data that can relevant to tracking events.
11+
*/
12+
export type TrackingEventDetails = {
13+
/**
14+
* A numeric value associated with this event.
15+
*/
16+
value?: number;
17+
} & Record<string, TrackingEventValue>;

packages/shared/src/types/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './metadata';
2-
export * from './paradigm';
2+
export * from './paradigm';
3+
export * from './structure';
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type PrimitiveValue = null | boolean | string | number;
2+
export type JsonObject = { [key: string]: JsonValue };
3+
export type JsonArray = JsonValue[];
4+
/**
5+
* Represents a JSON node value.
6+
*/
7+
export type JsonValue = PrimitiveValue | JsonObject | JsonArray;

packages/web/src/client/client.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import type { ClientMetadata, EvaluationLifeCycle, Eventing, ManageLogger } from
22
import type { Features } from '../evaluation';
33
import type { ProviderStatus } from '../provider';
44
import type { ProviderEvents } from '../events';
5+
import type { Tracking } from '../tracking';
56

6-
export interface Client extends EvaluationLifeCycle<Client>, Features, ManageLogger<Client>, Eventing<ProviderEvents> {
7+
export interface Client
8+
extends EvaluationLifeCycle<Client>,
9+
Features,
10+
ManageLogger<Client>,
11+
Eventing<ProviderEvents>,
12+
Tracking {
713
readonly metadata: ClientMetadata;
814
/**
915
* Returns the status of the associated provider.

packages/web/src/client/internal/open-feature-client.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import type {
88
HookContext,
99
JsonValue,
1010
Logger,
11+
TrackingEventDetails,
1112
OpenFeatureError,
12-
ResolutionDetails} from '@openfeature/core';
13+
ResolutionDetails } from '@openfeature/core';
1314
import {
1415
ErrorCode,
1516
ProviderFatalError,
@@ -181,6 +182,21 @@ export class OpenFeatureClient implements Client {
181182
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', options);
182183
}
183184

185+
track(occurrenceKey: string, occurrenceDetails: TrackingEventDetails): void {
186+
187+
this.shortCircuitIfNotReady();
188+
189+
const context = {
190+
...OpenFeature.getContext(this?.options?.domain),
191+
};
192+
193+
if (typeof this._provider.track === 'function') {
194+
return this._provider.track?.(occurrenceKey, context, occurrenceDetails);
195+
} else {
196+
this._logger.debug('Provider does not implement track function: will no-op.');
197+
}
198+
}
199+
184200
private evaluate<T extends FlagValue>(
185201
flagKey: string,
186202
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
@@ -217,12 +233,7 @@ export class OpenFeatureClient implements Client {
217233
try {
218234
this.beforeHooks(allHooks, hookContext, options);
219235

220-
// short circuit evaluation entirely if provider is in a bad state
221-
if (this.providerStatus === ProviderStatus.NOT_READY) {
222-
throw new ProviderNotReadyError('provider has not yet initialized');
223-
} else if (this.providerStatus === ProviderStatus.FATAL) {
224-
throw new ProviderFatalError('provider is in an irrecoverable error state');
225-
}
236+
this.shortCircuitIfNotReady();
226237

227238
// run the referenced resolver, binding the provider.
228239
const resolution = resolver.call(this._provider, flagKey, defaultValue, context, this._logger);
@@ -317,4 +328,13 @@ export class OpenFeatureClient implements Client {
317328
private get _logger() {
318329
return this._clientLogger || this.globalLogger();
319330
}
331+
332+
private shortCircuitIfNotReady() {
333+
// short circuit evaluation entirely if provider is in a bad state
334+
if (this.providerStatus === ProviderStatus.NOT_READY) {
335+
throw new ProviderNotReadyError('provider has not yet initialized');
336+
} else if (this.providerStatus === ProviderStatus.FATAL) {
337+
throw new ProviderFatalError('provider is in an irrecoverable error state');
338+
}
339+
}
320340
}

packages/web/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export * from './evaluation';
44
export * from './open-feature';
55
export * from './events';
66
export * from './hooks';
7+
export * from './tracking';
78
export * from '@openfeature/core';

packages/web/src/provider/provider.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
1+
import type { CommonProvider, EvaluationContext, JsonValue, Logger, TrackingEventDetails, ResolutionDetails } from '@openfeature/core';
22
import { ClientProviderStatus } from '@openfeature/core';
33
import type { Hook } from '../hooks';
44

@@ -72,4 +72,12 @@ export interface Provider extends CommonProvider<ClientProviderStatus> {
7272
context: EvaluationContext,
7373
logger: Logger,
7474
): ResolutionDetails<T>;
75+
76+
/**
77+
* Track a user action or application state, usually representing a business objective or outcome.
78+
* @param trackingEventName
79+
* @param context
80+
* @param trackingEventDetails
81+
*/
82+
track?(trackingEventName: string, context?: EvaluationContext, trackingEventDetails?: TrackingEventDetails): void;
7583
}

packages/web/src/tracking/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tracking';

packages/web/src/tracking/tracking.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { TrackingEventDetails } from '@openfeature/core';
2+
3+
export interface Tracking {
4+
5+
/**
6+
* Track a user action or application state, usually representing a business objective or outcome.
7+
* @param trackingEventName an identifier for the event
8+
* @param trackingEventDetails the details of the tracking event
9+
*/
10+
track(trackingEventName: string, trackingEventDetails?: TrackingEventDetails): void;
11+
}

0 commit comments

Comments
 (0)