@grafana-experiments/sdk 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +339 -0
- package/RELEASING.md +36 -0
- package/dist/chunk-W7444I4G.js +792 -0
- package/dist/grafana.cjs +852 -0
- package/dist/grafana.d.cts +22 -0
- package/dist/grafana.d.ts +22 -0
- package/dist/grafana.js +49 -0
- package/dist/index.cjs +846 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +26 -0
- package/dist/react.cjs +63 -0
- package/dist/react.d.cts +14 -0
- package/dist/react.d.ts +14 -0
- package/dist/react.js +36 -0
- package/dist/types-M9Kqiiqw.d.cts +171 -0
- package/dist/types-M9Kqiiqw.d.ts +171 -0
- package/package.json +73 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createExperimentEnricher: () => createExperimentEnricher,
|
|
34
|
+
createExperiments: () => createExperiments,
|
|
35
|
+
createTypedReporter: () => createTypedReporter,
|
|
36
|
+
defineEvent: () => defineEvent,
|
|
37
|
+
toOpenFeatureTrackingDetails: () => toOpenFeatureTrackingDetails
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/experiments.ts
|
|
42
|
+
var import_web_sdk2 = require("@openfeature/web-sdk");
|
|
43
|
+
|
|
44
|
+
// src/faro.ts
|
|
45
|
+
function createFaroReporter(faro) {
|
|
46
|
+
let owned;
|
|
47
|
+
let sequence = 0;
|
|
48
|
+
function finish(reason, reportingEnabled = true) {
|
|
49
|
+
const active = faro.api.getActiveUserAction();
|
|
50
|
+
if (!owned || active !== owned) {
|
|
51
|
+
owned = void 0;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const action = owned;
|
|
55
|
+
owned = void 0;
|
|
56
|
+
if (!reportingEnabled) {
|
|
57
|
+
action.cancel();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
action.attributes = { ...action.attributes, end_reason: reason };
|
|
61
|
+
action.end();
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
report(name, properties, type) {
|
|
65
|
+
const attributes = stringifyAttributes(properties);
|
|
66
|
+
if (type === "custom-event") {
|
|
67
|
+
faro.api.pushEvent(name, attributes, void 0, { skipDedupe: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!faro.instrumentations.instrumentations.some(
|
|
71
|
+
(item) => item.name === "@grafana/faro-web-sdk:instrumentation-user-action"
|
|
72
|
+
)) {
|
|
73
|
+
faro.api.pushEvent(
|
|
74
|
+
name,
|
|
75
|
+
{ ...attributes, action_fallback: "instrumentation-unavailable" },
|
|
76
|
+
void 0,
|
|
77
|
+
{ skipDedupe: true }
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
finish("superseded");
|
|
82
|
+
if (faro.api.getActiveUserAction()) {
|
|
83
|
+
faro.api.pushEvent(name, { ...attributes, action_fallback: "host-action-active" }, void 0, {
|
|
84
|
+
skipDedupe: true
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
owned = faro.api.startUserAction(name, {
|
|
89
|
+
...attributes,
|
|
90
|
+
experiment_action_sequence: String(sequence++)
|
|
91
|
+
});
|
|
92
|
+
if (!owned) {
|
|
93
|
+
faro.api.pushEvent(name, { ...attributes, action_fallback: "start-unavailable" }, void 0, {
|
|
94
|
+
skipDedupe: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
finish
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function stringifyAttributes(properties) {
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
104
|
+
if (value === void 0 || value === null) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
109
|
+
if (text !== void 0) {
|
|
110
|
+
result[key] = text;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/events.ts
|
|
119
|
+
function defineEvent(definition) {
|
|
120
|
+
return definition;
|
|
121
|
+
}
|
|
122
|
+
function createTypedReporter(sdk, definitions) {
|
|
123
|
+
const installed = sdk.getEventManifest().events;
|
|
124
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
125
|
+
if (JSON.stringify(installed[name]) !== JSON.stringify(definition))
|
|
126
|
+
throw new Error(`Event contract not installed: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
return (name, properties, options) => sdk.reportAnalytics(name, properties, options);
|
|
129
|
+
}
|
|
130
|
+
function validateEvent(definition, properties) {
|
|
131
|
+
if (!definition) return [];
|
|
132
|
+
return Object.entries(definition.properties).flatMap(([key, rule]) => {
|
|
133
|
+
const value = properties[key];
|
|
134
|
+
return value === void 0 && !rule.required ? [] : typeof value !== rule.type || typeof value === "number" && !Number.isFinite(value) ? [key] : [];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/enrichment.ts
|
|
139
|
+
function createExperimentEnricher(options) {
|
|
140
|
+
return (input) => {
|
|
141
|
+
let item = input;
|
|
142
|
+
if (options.isEnabled?.() !== false && ["event", "exception", "measurement"].includes(input.type)) {
|
|
143
|
+
const payload = input.payload;
|
|
144
|
+
const key = input.type === "event" ? "attributes" : "context";
|
|
145
|
+
const attributes = { ...payload[key] };
|
|
146
|
+
if (!("experiments" in attributes)) {
|
|
147
|
+
const timestamp = Date.parse(String(payload.timestamp));
|
|
148
|
+
const measurementStart = Number(attributes.experiment_started_at);
|
|
149
|
+
const origin = input.type === "measurement" ? measurementStart : timestamp;
|
|
150
|
+
const assignments = options.getAssignments().filter((a) => Number.isFinite(origin) && a.activated_at && Date.parse(a.activated_at) <= origin);
|
|
151
|
+
attributes.experiments = JSON.stringify(assignments);
|
|
152
|
+
}
|
|
153
|
+
attributes.event_id ||= crypto.randomUUID();
|
|
154
|
+
attributes.telemetry_schema_version = "2";
|
|
155
|
+
item = { ...input, payload: { ...payload, [key]: attributes } };
|
|
156
|
+
}
|
|
157
|
+
return options.beforeSend ? options.beforeSend(item) : item;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/semconv.ts
|
|
162
|
+
function featureFlagAttributes(flagKey, details, options) {
|
|
163
|
+
const attributes = { "feature_flag.key": flagKey };
|
|
164
|
+
if (details.variant) {
|
|
165
|
+
attributes["feature_flag.result.variant"] = details.variant;
|
|
166
|
+
} else if (options.recordValue) {
|
|
167
|
+
attributes["feature_flag.result.value"] = details.value;
|
|
168
|
+
}
|
|
169
|
+
if (details.reason) {
|
|
170
|
+
attributes["feature_flag.result.reason"] = details.reason.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
if (options.providerName) {
|
|
173
|
+
attributes["feature_flag.provider.name"] = options.providerName;
|
|
174
|
+
}
|
|
175
|
+
for (const [metadataKey, attribute] of [
|
|
176
|
+
["contextId", "feature_flag.context.id"],
|
|
177
|
+
["flagSetId", "feature_flag.set.id"],
|
|
178
|
+
["version", "feature_flag.version"]
|
|
179
|
+
]) {
|
|
180
|
+
const value = details.flagMetadata?.[metadataKey];
|
|
181
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
182
|
+
attributes[attribute] = String(value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return attributes;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/readiness.ts
|
|
189
|
+
var import_web_sdk = require("@openfeature/web-sdk");
|
|
190
|
+
function waitForReady(client, timeout, signal) {
|
|
191
|
+
if (signal?.aborted) {
|
|
192
|
+
return Promise.resolve(false);
|
|
193
|
+
}
|
|
194
|
+
if (client.providerStatus === import_web_sdk.ClientProviderStatus.READY) {
|
|
195
|
+
return Promise.resolve(true);
|
|
196
|
+
}
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
let timer;
|
|
199
|
+
const finish = (ready) => {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
client.removeHandler(import_web_sdk.ProviderEvents.Ready, onReady);
|
|
202
|
+
client.removeHandler(import_web_sdk.ProviderEvents.Error, onError);
|
|
203
|
+
signal?.removeEventListener("abort", onError);
|
|
204
|
+
resolve(ready);
|
|
205
|
+
};
|
|
206
|
+
const onReady = () => finish(true);
|
|
207
|
+
const onError = () => finish(false);
|
|
208
|
+
client.addHandler(import_web_sdk.ProviderEvents.Ready, onReady);
|
|
209
|
+
client.addHandler(import_web_sdk.ProviderEvents.Error, onError);
|
|
210
|
+
signal?.addEventListener("abort", onError, { once: true });
|
|
211
|
+
timer = setTimeout(onError, timeout);
|
|
212
|
+
if (client.providerStatus === import_web_sdk.ClientProviderStatus.READY) {
|
|
213
|
+
onReady();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/experiments.ts
|
|
219
|
+
var INACTIVE = Object.freeze({ status: "inactive" });
|
|
220
|
+
function createExperiments(options) {
|
|
221
|
+
if (!options.scope.trim()) {
|
|
222
|
+
throw new Error("An application scope is required");
|
|
223
|
+
}
|
|
224
|
+
const timeout = options.readinessTimeoutMs ?? 1e4;
|
|
225
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
226
|
+
throw new Error("readinessTimeoutMs must be positive and finite");
|
|
227
|
+
}
|
|
228
|
+
let disposed = false;
|
|
229
|
+
let contextKey = options.contextKey ?? "default";
|
|
230
|
+
let sessionId;
|
|
231
|
+
let generation = 0;
|
|
232
|
+
let identityGeneration = 0;
|
|
233
|
+
const diagnostics = [];
|
|
234
|
+
let diagnosticSequence = 0;
|
|
235
|
+
const diagnosticListeners = /* @__PURE__ */ new Set();
|
|
236
|
+
function diagnose(code, fields = {}) {
|
|
237
|
+
const diagnostic = Object.freeze({
|
|
238
|
+
sequence: ++diagnosticSequence,
|
|
239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
code,
|
|
241
|
+
...fields
|
|
242
|
+
});
|
|
243
|
+
diagnostics.push(diagnostic);
|
|
244
|
+
if (diagnostics.length > 200) diagnostics.shift();
|
|
245
|
+
for (const listener of [options.onDiagnostic, ...diagnosticListeners]) {
|
|
246
|
+
try {
|
|
247
|
+
listener?.(diagnostic);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let faro = options.faro.instance;
|
|
253
|
+
let mirror = faro ? createFaroReporter(faro) : void 0;
|
|
254
|
+
let initializing;
|
|
255
|
+
let pausedByConsent = false;
|
|
256
|
+
const lifetime = new AbortController();
|
|
257
|
+
const entries = /* @__PURE__ */ new Map();
|
|
258
|
+
function error(value) {
|
|
259
|
+
diagnose("reporting-error", { detail: value instanceof Error ? value.message : String(value) });
|
|
260
|
+
try {
|
|
261
|
+
options.onError?.(value);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function attempt(work) {
|
|
266
|
+
try {
|
|
267
|
+
void Promise.resolve(work()).catch(error);
|
|
268
|
+
} catch (value) {
|
|
269
|
+
error(value);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function enabled() {
|
|
273
|
+
try {
|
|
274
|
+
return !disposed && (options.isEnabled?.() ?? true);
|
|
275
|
+
} catch (value) {
|
|
276
|
+
error(value);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function storage() {
|
|
281
|
+
if (options.storage !== void 0) {
|
|
282
|
+
return options.storage;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
return typeof sessionStorage === "undefined" ? null : sessionStorage;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function resolveSession() {
|
|
291
|
+
try {
|
|
292
|
+
const host = options.getSessionId?.();
|
|
293
|
+
if (typeof host === "string" && host.trim()) {
|
|
294
|
+
return host;
|
|
295
|
+
}
|
|
296
|
+
} catch (value) {
|
|
297
|
+
error(value);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const fallback = faro?.api.getSession()?.id;
|
|
301
|
+
return typeof fallback === "string" && fallback.trim() ? fallback : void 0;
|
|
302
|
+
} catch (value) {
|
|
303
|
+
error(value);
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function syncSession() {
|
|
308
|
+
const allowed = enabled();
|
|
309
|
+
if (faro && options.faro.config) {
|
|
310
|
+
attempt(() => {
|
|
311
|
+
if (!allowed && !faro.transports.isPaused()) {
|
|
312
|
+
faro.pause();
|
|
313
|
+
pausedByConsent = true;
|
|
314
|
+
} else if (allowed && pausedByConsent) {
|
|
315
|
+
faro.unpause();
|
|
316
|
+
pausedByConsent = false;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const next = allowed ? resolveSession() : void 0;
|
|
321
|
+
if (next === sessionId) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
sessionId = next;
|
|
325
|
+
generation++;
|
|
326
|
+
identityGeneration++;
|
|
327
|
+
for (const entry of entries.values()) {
|
|
328
|
+
entry.reset();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const onMetaChange = () => {
|
|
332
|
+
attempt(syncSession);
|
|
333
|
+
};
|
|
334
|
+
const onProviderReady = () => {
|
|
335
|
+
for (const entry of entries.values()) {
|
|
336
|
+
entry.retry();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
options.client.addHandler(import_web_sdk2.ProviderEvents.Ready, onProviderReady);
|
|
340
|
+
options.client.addHandler(import_web_sdk2.ProviderEvents.ConfigurationChanged, onProviderReady);
|
|
341
|
+
faro?.metas.addListener(onMetaChange);
|
|
342
|
+
async function initialize() {
|
|
343
|
+
if (faro || disposed || !enabled()) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (initializing) {
|
|
347
|
+
return initializing;
|
|
348
|
+
}
|
|
349
|
+
initializing = (async () => {
|
|
350
|
+
try {
|
|
351
|
+
const { initializeFaro, UserActionInstrumentation } = await import("@grafana/faro-web-sdk");
|
|
352
|
+
if (disposed || !enabled() || !options.faro.config) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const configured = options.faro.config.instrumentations;
|
|
356
|
+
const instrumentations = configured && !configured.some((item) => item.name === "@grafana/faro-web-sdk:instrumentation-user-action") ? [...configured, new UserActionInstrumentation()] : configured;
|
|
357
|
+
const initialized = initializeFaro({
|
|
358
|
+
preventGlobalExposure: true,
|
|
359
|
+
...options.faro.config,
|
|
360
|
+
isolate: true,
|
|
361
|
+
instrumentations,
|
|
362
|
+
beforeSend: options.enrichFaro ? createExperimentEnricher({
|
|
363
|
+
getAssignments: () => sdk.getActiveAssignments(),
|
|
364
|
+
isEnabled: enabled,
|
|
365
|
+
beforeSend: options.faro.config.beforeSend
|
|
366
|
+
}) : options.faro.config.beforeSend
|
|
367
|
+
});
|
|
368
|
+
if (!initialized) {
|
|
369
|
+
throw new Error("Faro initialization did not return an instance");
|
|
370
|
+
}
|
|
371
|
+
faro = initialized;
|
|
372
|
+
mirror = createFaroReporter(faro);
|
|
373
|
+
faro.metas.addListener(onMetaChange);
|
|
374
|
+
syncSession();
|
|
375
|
+
} catch (value) {
|
|
376
|
+
error(value);
|
|
377
|
+
}
|
|
378
|
+
})();
|
|
379
|
+
await initializing;
|
|
380
|
+
initializing = void 0;
|
|
381
|
+
}
|
|
382
|
+
function sendFaro(name, properties, type) {
|
|
383
|
+
const report2 = () => {
|
|
384
|
+
if (!enabled()) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const transformed = options.transformFaroProperties ? options.transformFaroProperties(name, properties) : properties;
|
|
388
|
+
if (transformed !== null) {
|
|
389
|
+
if (mirror) {
|
|
390
|
+
diagnose("faro-attempted", { eventName: name });
|
|
391
|
+
mirror.report(name, transformed, type);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
diagnose("faro-filtered", { eventName: name });
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
attempt(() => mirror ? report2() : initialize().then(report2));
|
|
398
|
+
}
|
|
399
|
+
function copyProperties(properties) {
|
|
400
|
+
const result = {};
|
|
401
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
402
|
+
try {
|
|
403
|
+
result[key] = structuredClone(value);
|
|
404
|
+
} catch (value2) {
|
|
405
|
+
error(value2);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
const sdk = {
|
|
411
|
+
ready: initialize(),
|
|
412
|
+
defineExperiment(definition) {
|
|
413
|
+
if (disposed) {
|
|
414
|
+
throw new Error("Experiments instance is disposed");
|
|
415
|
+
}
|
|
416
|
+
if (!definition.id.trim() || !definition.flagKey.trim()) {
|
|
417
|
+
throw new Error("Experiment id and flagKey are required");
|
|
418
|
+
}
|
|
419
|
+
if (entries.has(definition.id)) {
|
|
420
|
+
throw new Error(`Experiment already defined: ${definition.id}`);
|
|
421
|
+
}
|
|
422
|
+
const { id, flagKey, group = "rollout", flag } = definition;
|
|
423
|
+
let state = INACTIVE;
|
|
424
|
+
let entryId = "default";
|
|
425
|
+
let entrySettled = false;
|
|
426
|
+
let entryGeneration = 0;
|
|
427
|
+
let history = {};
|
|
428
|
+
let restored;
|
|
429
|
+
const settled = () => entrySettled && (state.status === "active" || state.status === "excluded");
|
|
430
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
431
|
+
const update = (next) => {
|
|
432
|
+
state = Object.freeze(next);
|
|
433
|
+
for (const listener of listeners) {
|
|
434
|
+
attempt(listener);
|
|
435
|
+
}
|
|
436
|
+
return state;
|
|
437
|
+
};
|
|
438
|
+
const key = () => `grafana-experiments:v2:${JSON.stringify([options.scope, contextKey, sessionId, id, flagKey, flag.type])}`;
|
|
439
|
+
function unavailable(reason) {
|
|
440
|
+
diagnose("activation-unavailable", { experimentId: id, detail: reason });
|
|
441
|
+
return update({ status: "unavailable", reason });
|
|
442
|
+
}
|
|
443
|
+
function parse(value) {
|
|
444
|
+
if (flag.type === "boolean") {
|
|
445
|
+
return typeof value === "boolean" ? { variant: value ? "treatment" : "control" } : void 0;
|
|
446
|
+
}
|
|
447
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const variant = value.variant;
|
|
451
|
+
if (!["control", "treatment", "excluded"].includes(variant) || !flag.validate(value)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
return { variant, config: structuredClone(value) };
|
|
455
|
+
}
|
|
456
|
+
function assigned(parsed, metadata = {}, source = "unknown") {
|
|
457
|
+
if (parsed.variant === "excluded") {
|
|
458
|
+
return { status: "excluded", ...parsed, variant: "excluded" };
|
|
459
|
+
}
|
|
460
|
+
const unit = metadata.allocationUnit;
|
|
461
|
+
if (unit && (!["stack", "organization", "user", "session"].includes(unit.type) || !unit.id?.trim()))
|
|
462
|
+
throw new Error("Invalid allocation unit");
|
|
463
|
+
const signature = JSON.stringify([
|
|
464
|
+
parsed.variant,
|
|
465
|
+
metadata.revision,
|
|
466
|
+
unit?.type,
|
|
467
|
+
unit?.id,
|
|
468
|
+
metadata.population
|
|
469
|
+
]);
|
|
470
|
+
const previous = history[signature];
|
|
471
|
+
const assignment = previous ?? {
|
|
472
|
+
experiment_id: id,
|
|
473
|
+
experiment_group: group,
|
|
474
|
+
flag_key: flagKey,
|
|
475
|
+
variant: parsed.variant,
|
|
476
|
+
session_id: sessionId,
|
|
477
|
+
exposure_id: crypto.randomUUID(),
|
|
478
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
479
|
+
experiment_revision: metadata.revision,
|
|
480
|
+
allocation_unit_type: unit?.type,
|
|
481
|
+
allocation_unit_id: unit?.id,
|
|
482
|
+
population: metadata.population,
|
|
483
|
+
assignment_source: source
|
|
484
|
+
};
|
|
485
|
+
history[signature] = assignment;
|
|
486
|
+
return {
|
|
487
|
+
status: "active",
|
|
488
|
+
...parsed,
|
|
489
|
+
variant: parsed.variant,
|
|
490
|
+
assignment: Object.freeze(assignment)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
entries.set(id, {
|
|
494
|
+
reset: () => {
|
|
495
|
+
entrySettled = false;
|
|
496
|
+
entryGeneration++;
|
|
497
|
+
history = {};
|
|
498
|
+
restored = void 0;
|
|
499
|
+
update(INACTIVE);
|
|
500
|
+
},
|
|
501
|
+
retry: () => {
|
|
502
|
+
if (state.status === "unavailable" && ["provider", "evaluation", "invalid-value"].includes(state.reason)) {
|
|
503
|
+
update(INACTIVE);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
assignment: () => state.status === "active" ? state.assignment : void 0,
|
|
507
|
+
storageKey: key
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
id,
|
|
511
|
+
getSnapshot: () => state,
|
|
512
|
+
subscribe(listener) {
|
|
513
|
+
listeners.add(listener);
|
|
514
|
+
return () => {
|
|
515
|
+
listeners.delete(listener);
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
async activate({ signal, entryId: requestedEntry } = {}) {
|
|
519
|
+
if (requestedEntry !== void 0 && requestedEntry !== entryId) {
|
|
520
|
+
entryId = requestedEntry;
|
|
521
|
+
entryGeneration++;
|
|
522
|
+
entrySettled = false;
|
|
523
|
+
}
|
|
524
|
+
const thisEntry = entryGeneration;
|
|
525
|
+
if (signal?.aborted) {
|
|
526
|
+
return INACTIVE;
|
|
527
|
+
}
|
|
528
|
+
if (disposed) {
|
|
529
|
+
return unavailable("disposed");
|
|
530
|
+
}
|
|
531
|
+
await initialize();
|
|
532
|
+
syncSession();
|
|
533
|
+
if (!enabled()) {
|
|
534
|
+
return unavailable("disabled");
|
|
535
|
+
}
|
|
536
|
+
if (!sessionId) {
|
|
537
|
+
return unavailable("no-session");
|
|
538
|
+
}
|
|
539
|
+
if (signal?.aborted) {
|
|
540
|
+
return INACTIVE;
|
|
541
|
+
}
|
|
542
|
+
if (settled()) {
|
|
543
|
+
diagnose("exposure-suppressed", { experimentId: id, detail: "same-entry" });
|
|
544
|
+
return state;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const cached = storage()?.getItem(key());
|
|
548
|
+
if (cached) {
|
|
549
|
+
const saved = JSON.parse(cached);
|
|
550
|
+
if (saved.sessionId === sessionId && saved.schemaVersion === 2) {
|
|
551
|
+
history = saved.history || {};
|
|
552
|
+
restored = saved;
|
|
553
|
+
const parsed = parse(saved.value);
|
|
554
|
+
if (requestedEntry === void 0 && parsed) {
|
|
555
|
+
entrySettled = true;
|
|
556
|
+
diagnose("exposure-suppressed", { experimentId: id, detail: "session-restored" });
|
|
557
|
+
return update(
|
|
558
|
+
parsed.variant === "excluded" ? { status: "excluded", ...parsed, variant: "excluded" } : {
|
|
559
|
+
status: "active",
|
|
560
|
+
...parsed,
|
|
561
|
+
variant: parsed.variant,
|
|
562
|
+
assignment: Object.freeze(saved.assignment)
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} catch (value) {
|
|
569
|
+
error(value);
|
|
570
|
+
}
|
|
571
|
+
const startGeneration = generation;
|
|
572
|
+
const activationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
573
|
+
let ready = false;
|
|
574
|
+
try {
|
|
575
|
+
ready = await waitForReady(options.client, timeout, activationSignal);
|
|
576
|
+
} catch (value) {
|
|
577
|
+
error(value);
|
|
578
|
+
}
|
|
579
|
+
syncSession();
|
|
580
|
+
if (activationSignal.aborted || startGeneration !== generation || thisEntry !== entryGeneration) {
|
|
581
|
+
return INACTIVE;
|
|
582
|
+
}
|
|
583
|
+
if (!enabled()) {
|
|
584
|
+
return unavailable("disabled");
|
|
585
|
+
}
|
|
586
|
+
if (!ready || options.client.providerStatus !== import_web_sdk2.ClientProviderStatus.READY) {
|
|
587
|
+
return unavailable("provider");
|
|
588
|
+
}
|
|
589
|
+
if (settled()) {
|
|
590
|
+
return state;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const details = flag.type === "boolean" ? options.client.getBooleanDetails(flagKey, false) : options.client.getObjectDetails(flagKey, { variant: "excluded" });
|
|
594
|
+
if (details.errorCode) {
|
|
595
|
+
return unavailable("evaluation");
|
|
596
|
+
}
|
|
597
|
+
const parsed = parse(details.value);
|
|
598
|
+
if (!parsed) {
|
|
599
|
+
return unavailable("invalid-value");
|
|
600
|
+
}
|
|
601
|
+
const providerMetadata = definition.mapAssignment?.(details);
|
|
602
|
+
const metadata = { ...definition.metadata, ...providerMetadata };
|
|
603
|
+
const priorIds = new Set(Object.values(history).map((a) => a.exposure_id));
|
|
604
|
+
const previous = state.status === "active" ? state.assignment : restored?.assignment;
|
|
605
|
+
const next = assigned(
|
|
606
|
+
parsed,
|
|
607
|
+
metadata,
|
|
608
|
+
providerMetadata?.allocationUnit ? "provider" : definition.metadata?.allocationUnit ? "host" : "unknown"
|
|
609
|
+
);
|
|
610
|
+
entrySettled = true;
|
|
611
|
+
diagnose("evaluated", {
|
|
612
|
+
experimentId: id,
|
|
613
|
+
detail: `${parsed.variant}; ${details.reason ?? "unknown"}`
|
|
614
|
+
});
|
|
615
|
+
if (previous && (next.status !== "active" || previous.exposure_id !== next.assignment.exposure_id)) {
|
|
616
|
+
diagnose("assignment-transition", {
|
|
617
|
+
experimentId: id,
|
|
618
|
+
detail: `${previous.variant} \u2192 ${parsed.variant}`
|
|
619
|
+
});
|
|
620
|
+
sendFaro(
|
|
621
|
+
"experiment_assignment_changed",
|
|
622
|
+
{
|
|
623
|
+
experiment_id: id,
|
|
624
|
+
previous_exposure_id: previous.exposure_id,
|
|
625
|
+
variant: parsed.variant,
|
|
626
|
+
event_id: crypto.randomUUID(),
|
|
627
|
+
telemetry_schema_version: 2
|
|
628
|
+
},
|
|
629
|
+
"custom-event"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
state = Object.freeze(next);
|
|
633
|
+
try {
|
|
634
|
+
storage()?.setItem(
|
|
635
|
+
key(),
|
|
636
|
+
JSON.stringify({
|
|
637
|
+
schemaVersion: 2,
|
|
638
|
+
sessionId,
|
|
639
|
+
value: details.value,
|
|
640
|
+
assignment: next.status === "active" ? next.assignment : void 0,
|
|
641
|
+
history
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
} catch (value) {
|
|
645
|
+
error(value);
|
|
646
|
+
}
|
|
647
|
+
if (next.status === "active" && !priorIds.has(next.assignment.exposure_id)) {
|
|
648
|
+
diagnose("activated", { experimentId: id });
|
|
649
|
+
const properties = {
|
|
650
|
+
...next.assignment,
|
|
651
|
+
...featureFlagAttributes(flagKey, details, {
|
|
652
|
+
providerName: options.client.metadata.providerMetadata?.name,
|
|
653
|
+
recordValue: options.recordFlagValue ?? true
|
|
654
|
+
}),
|
|
655
|
+
event_id: crypto.randomUUID(),
|
|
656
|
+
telemetry_schema_version: 2
|
|
657
|
+
};
|
|
658
|
+
attempt(
|
|
659
|
+
() => options.reportExposure ? options.reportExposure(next.assignment) : options.reportAnalytics?.("experiment_viewed", { ...properties })
|
|
660
|
+
);
|
|
661
|
+
sendFaro("experiment_viewed", properties, "custom-event");
|
|
662
|
+
} else {
|
|
663
|
+
diagnose("exposure-suppressed", {
|
|
664
|
+
experimentId: id,
|
|
665
|
+
detail: next.status === "excluded" ? "excluded" : "already-exposed"
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return update(next);
|
|
669
|
+
} catch (value) {
|
|
670
|
+
error(value);
|
|
671
|
+
return unavailable("evaluation");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
},
|
|
676
|
+
reportAnalytics(name, properties = {}, reportOptions) {
|
|
677
|
+
report(name, properties, reportOptions);
|
|
678
|
+
},
|
|
679
|
+
getActiveAssignments() {
|
|
680
|
+
syncSession();
|
|
681
|
+
return [...entries.values()].flatMap((entry) => {
|
|
682
|
+
const assignment = entry.assignment();
|
|
683
|
+
return assignment ? [structuredClone(assignment)] : [];
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
getEventManifest: () => structuredClone({ schemaVersion: 2, scope: options.scope, events: options.eventDefinitions ?? {} }),
|
|
687
|
+
getDiagnostics: () => [...diagnostics],
|
|
688
|
+
subscribeDiagnostics(listener) {
|
|
689
|
+
diagnosticListeners.add(listener);
|
|
690
|
+
return () => {
|
|
691
|
+
diagnosticListeners.delete(listener);
|
|
692
|
+
};
|
|
693
|
+
},
|
|
694
|
+
beginJourney() {
|
|
695
|
+
const assignments = sdk.getActiveAssignments();
|
|
696
|
+
const origin = identityGeneration;
|
|
697
|
+
const id = crypto.randomUUID();
|
|
698
|
+
let ended = false;
|
|
699
|
+
return {
|
|
700
|
+
id,
|
|
701
|
+
getTelemetryContext() {
|
|
702
|
+
syncSession();
|
|
703
|
+
return {
|
|
704
|
+
experiments: JSON.stringify(
|
|
705
|
+
!ended && origin === identityGeneration && enabled() ? assignments : []
|
|
706
|
+
),
|
|
707
|
+
journey_id: id
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
end() {
|
|
711
|
+
ended = true;
|
|
712
|
+
},
|
|
713
|
+
reportAnalytics(name, properties = {}, reportOptions) {
|
|
714
|
+
syncSession();
|
|
715
|
+
if (ended || origin !== identityGeneration) {
|
|
716
|
+
diagnose("journey-invalidated", { eventName: name });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
report(name, properties, reportOptions, assignments, id);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
},
|
|
723
|
+
refreshSession() {
|
|
724
|
+
attempt(syncSession);
|
|
725
|
+
void initialize();
|
|
726
|
+
},
|
|
727
|
+
resetContext(nextContextKey) {
|
|
728
|
+
if (disposed) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
attempt(() => mirror?.finish("context-reset", enabled()));
|
|
732
|
+
if (nextContextKey === contextKey) {
|
|
733
|
+
for (const entry of entries.values()) {
|
|
734
|
+
attempt(() => storage()?.removeItem(entry.storageKey()));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
contextKey = nextContextKey;
|
|
738
|
+
generation++;
|
|
739
|
+
identityGeneration++;
|
|
740
|
+
for (const entry of entries.values()) {
|
|
741
|
+
entry.reset();
|
|
742
|
+
}
|
|
743
|
+
syncSession();
|
|
744
|
+
},
|
|
745
|
+
dispose() {
|
|
746
|
+
if (disposed) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
attempt(() => mirror?.finish("disposed", enabled()));
|
|
750
|
+
disposed = true;
|
|
751
|
+
lifetime.abort();
|
|
752
|
+
options.client.removeHandler(import_web_sdk2.ProviderEvents.Ready, onProviderReady);
|
|
753
|
+
options.client.removeHandler(import_web_sdk2.ProviderEvents.ConfigurationChanged, onProviderReady);
|
|
754
|
+
attempt(() => faro?.metas.removeListener(onMetaChange));
|
|
755
|
+
if (faro && options.faro.config) {
|
|
756
|
+
attempt(() => faro.pause());
|
|
757
|
+
attempt(() => faro.instrumentations.remove(...faro.instrumentations.instrumentations));
|
|
758
|
+
attempt(() => faro.transports.remove(...faro.transports.transports));
|
|
759
|
+
}
|
|
760
|
+
for (const entry of entries.values()) {
|
|
761
|
+
entry.reset();
|
|
762
|
+
}
|
|
763
|
+
entries.clear();
|
|
764
|
+
diagnosticListeners.clear();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
function report(name, properties, reportOptions, captured, journeyId) {
|
|
768
|
+
syncSession();
|
|
769
|
+
if (!enabled()) {
|
|
770
|
+
diagnose("consent-gated", { eventName: name });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const assignments = captured ?? sdk.getActiveAssignments();
|
|
774
|
+
const definition = options.eventDefinitions?.[name];
|
|
775
|
+
const invalid = validateEvent(definition, properties);
|
|
776
|
+
const context = {
|
|
777
|
+
experiments: structuredClone(assignments),
|
|
778
|
+
event_id: crypto.randomUUID(),
|
|
779
|
+
telemetry_schema_version: 2,
|
|
780
|
+
event_schema_version: definition?.version,
|
|
781
|
+
...journeyId ? { journey_id: journeyId } : {}
|
|
782
|
+
};
|
|
783
|
+
const enriched = {
|
|
784
|
+
...copyProperties(properties),
|
|
785
|
+
...structuredClone(context),
|
|
786
|
+
measurement_valid: invalid.length === 0
|
|
787
|
+
};
|
|
788
|
+
const copied = copyProperties(enriched);
|
|
789
|
+
if (options.reportAnalytics) {
|
|
790
|
+
diagnose("analytics-attempted", { eventName: name });
|
|
791
|
+
attempt(() => options.reportAnalytics(name, enriched));
|
|
792
|
+
}
|
|
793
|
+
if (invalid.length) {
|
|
794
|
+
diagnose("schema-invalid", { eventName: name, detail: invalid.join(", ") });
|
|
795
|
+
sendFaro(
|
|
796
|
+
"experiment_diagnostic",
|
|
797
|
+
{
|
|
798
|
+
code: "schema-invalid",
|
|
799
|
+
event_name: name,
|
|
800
|
+
experiments: assignments,
|
|
801
|
+
event_id: enriched.event_id,
|
|
802
|
+
telemetry_schema_version: 2
|
|
803
|
+
},
|
|
804
|
+
"custom-event"
|
|
805
|
+
);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (options.openFeatureTracking) {
|
|
809
|
+
attempt(() => {
|
|
810
|
+
const details = options.openFeatureTracking(
|
|
811
|
+
name,
|
|
812
|
+
copyProperties(properties),
|
|
813
|
+
structuredClone(context)
|
|
814
|
+
);
|
|
815
|
+
if (details) {
|
|
816
|
+
options.client.track(name, details);
|
|
817
|
+
diagnose("openfeature-track-attempted", { eventName: name });
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
sendFaro(name, copied, reportOptions?.faro?.type ?? options.events?.[name] ?? "custom-event");
|
|
822
|
+
}
|
|
823
|
+
syncSession();
|
|
824
|
+
return sdk;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/tracking.ts
|
|
828
|
+
function toOpenFeatureTrackingDetails(context) {
|
|
829
|
+
return {
|
|
830
|
+
event_id: context.event_id,
|
|
831
|
+
telemetry_schema_version: context.telemetry_schema_version,
|
|
832
|
+
experiments: context.experiments.map(
|
|
833
|
+
(assignment) => Object.fromEntries(Object.entries(assignment).filter(([, value]) => value !== void 0))
|
|
834
|
+
),
|
|
835
|
+
...context.event_schema_version !== void 0 ? { event_schema_version: context.event_schema_version } : {},
|
|
836
|
+
...context.journey_id !== void 0 ? { journey_id: context.journey_id } : {}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
840
|
+
0 && (module.exports = {
|
|
841
|
+
createExperimentEnricher,
|
|
842
|
+
createExperiments,
|
|
843
|
+
createTypedReporter,
|
|
844
|
+
defineEvent,
|
|
845
|
+
toOpenFeatureTrackingDetails
|
|
846
|
+
});
|