@configdirector/nuxt-sdk 0.7.0 → 0.9.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.
Files changed (24) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +9 -0
  3. package/dist/runtime/app/composables/useConfigDirectorClient.d.mts +1 -1
  4. package/dist/runtime/app/composables/useConfigDirectorClient.d.ts +1 -1
  5. package/dist/runtime/app/composables/useConfigDirectorClientHooks.d.mts +6 -0
  6. package/dist/runtime/app/composables/useConfigDirectorClientHooks.d.ts +6 -0
  7. package/dist/runtime/app/composables/useConfigDirectorClientHooks.mjs +22 -0
  8. package/dist/runtime/app/composables/useConfigDirectorContext.d.mts +1 -1
  9. package/dist/runtime/app/composables/useConfigDirectorContext.d.ts +1 -1
  10. package/dist/runtime/app/composables/useConfigDirectorValue.d.mts +1 -1
  11. package/dist/runtime/app/composables/useConfigDirectorValue.d.ts +1 -1
  12. package/dist/runtime/nitro/plugin.d.mts +1 -1
  13. package/dist/runtime/nitro/plugin.d.ts +1 -1
  14. package/dist/runtime/nitro/plugin.mjs +126 -42
  15. package/dist/runtime/plugin.client.mjs +110 -32
  16. package/dist/runtime/server/composables/useConfigDirectorClient.d.mts +1 -1
  17. package/dist/runtime/server/composables/useConfigDirectorClient.d.ts +1 -1
  18. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.mts +6 -0
  19. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.ts +6 -0
  20. package/dist/runtime/server/composables/useConfigDirectorServerHooks.mjs +14 -0
  21. package/dist/runtime/{types-BkRZRTVv.d.mts → types-B4EK1B6g.d.mts} +12 -2
  22. package/dist/runtime/{types-f2z6DgES.d.mts → types-CHmsCChN.d.mts} +11 -1
  23. package/dist/runtime/{types-BDwzL0dp.d.mts → types-CzmBIfqo.d.mts} +11 -2
  24. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "0.7.0"
7
+ "version": "0.9.0"
8
8
  }
package/dist/module.mjs CHANGED
@@ -37,6 +37,10 @@ var module_default = defineNuxtModule({
37
37
  name: "useConfigDirectorClient",
38
38
  as: "useConfigDirectorClient",
39
39
  from: resolver.resolve("./runtime/server/composables/useConfigDirectorClient")
40
+ }, {
41
+ name: "useConfigDirectorServerHooks",
42
+ as: "useConfigDirectorServerHooks",
43
+ from: resolver.resolve("./runtime/server/composables/useConfigDirectorServerHooks")
40
44
  }]);
41
45
  addPlugin(resolver.resolve("./runtime/plugin.server"));
42
46
  addPlugin(resolver.resolve("./runtime/plugin.client"));
@@ -60,6 +64,11 @@ var module_default = defineNuxtModule({
60
64
  name: "useConfigDirectorValue",
61
65
  as: "useConfigDirectorValue",
62
66
  from: resolver.resolve("./runtime/app/composables/useConfigDirectorValue")
67
+ },
68
+ {
69
+ name: "useConfigDirectorClientHooks",
70
+ as: "useConfigDirectorClientHooks",
71
+ from: resolver.resolve("./runtime/app/composables/useConfigDirectorClientHooks")
63
72
  }
64
73
  ]);
65
74
  }
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-BkRZRTVv.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-B4EK1B6g.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => {
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-BkRZRTVv.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-B4EK1B6g.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => {
@@ -0,0 +1,6 @@
1
+ import { t as ClientHooks } from "../../types-B4EK1B6g.mjs";
2
+
3
+ //#region src/runtime/app/composables/useConfigDirectorClientHooks.d.ts
4
+ declare const useConfigDirectorClientHooks: (hooks: ClientHooks) => void;
5
+ //#endregion
6
+ export { useConfigDirectorClientHooks };
@@ -0,0 +1,6 @@
1
+ import { t as ClientHooks } from "../../types-B4EK1B6g.mjs";
2
+
3
+ //#region src/runtime/app/composables/useConfigDirectorClientHooks.d.ts
4
+ declare const useConfigDirectorClientHooks: (hooks: ClientHooks) => void;
5
+ //#endregion
6
+ export { useConfigDirectorClientHooks };
@@ -0,0 +1,22 @@
1
+ import { useNuxtApp } from "#app";
2
+ import { getCurrentInstance, onUnmounted } from "vue";
3
+ //#region src/runtime/app/composables/useConfigDirectorClientHooks.ts
4
+ const useConfigDirectorClientHooks = (hooks) => {
5
+ const { $configDirectorClient } = useNuxtApp();
6
+ const client = $configDirectorClient;
7
+ const hookEntries = Object.entries(hooks);
8
+ for (const [event, handler] of hookEntries) {
9
+ if (!handler) continue;
10
+ const handlers = Array.isArray(handler) ? handler : [handler];
11
+ for (const h of handlers) client.on(event, h);
12
+ }
13
+ if (getCurrentInstance()) onUnmounted(() => {
14
+ for (const [event, handler] of hookEntries) {
15
+ if (!handler) continue;
16
+ const handlers = Array.isArray(handler) ? handler : [handler];
17
+ for (const h of handlers) client.off(event, h);
18
+ }
19
+ });
20
+ };
21
+ //#endregion
22
+ export { useConfigDirectorClientHooks };
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorContext } from "../../types-f2z6DgES.mjs";
1
+ import { t as ConfigDirectorContext } from "../../types-CHmsCChN.mjs";
2
2
  import { Ref } from "vue";
3
3
 
4
4
  //#region src/runtime/app/composables/useConfigDirectorContext.d.ts
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorContext } from "../../types-f2z6DgES.mjs";
1
+ import { t as ConfigDirectorContext } from "../../types-CHmsCChN.mjs";
2
2
  import { Ref } from "vue";
3
3
 
4
4
  //#region src/runtime/app/composables/useConfigDirectorContext.d.ts
@@ -1,4 +1,4 @@
1
- import { r as ConfigValueType } from "../../types-f2z6DgES.mjs";
1
+ import { i as ConfigValueType } from "../../types-CHmsCChN.mjs";
2
2
  import { t as ClientStatus } from "../../types-CkzBXJwA.mjs";
3
3
  import { DeepReadonly, Ref, ShallowRef } from "vue";
4
4
 
@@ -1,4 +1,4 @@
1
- import { r as ConfigValueType } from "../../types-f2z6DgES.mjs";
1
+ import { i as ConfigValueType } from "../../types-CHmsCChN.mjs";
2
2
  import { t as ClientStatus } from "../../types-CkzBXJwA.mjs";
3
3
  import { DeepReadonly, Ref, ShallowRef } from "vue";
4
4
 
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../types-BDwzL0dp.mjs";
1
+ import { n as ConfigDirectorClient } from "../types-CzmBIfqo.mjs";
2
2
  import * as _$nitropack from "nitropack";
3
3
 
4
4
  //#region src/runtime/nitro/plugin.d.ts
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../types-BDwzL0dp.mjs";
1
+ import { n as ConfigDirectorClient } from "../types-CzmBIfqo.mjs";
2
2
  import * as _$nitropack from "nitropack";
3
3
 
4
4
  //#region src/runtime/nitro/plugin.d.ts
@@ -2,6 +2,7 @@ import EventEmitter, { EventEmitter as EventEmitter$1 } from "node:events";
2
2
  import { findByPointer } from "@jsonjoy.com/json-pointer";
3
3
  import * as semver from "semver";
4
4
  import { rapidhash } from "rapidhash-js";
5
+ import { createHash } from "node:crypto";
5
6
  import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
6
7
  import { consola } from "consola";
7
8
  //#region ../shared/src/errors.ts
@@ -849,22 +850,25 @@ const parseConfigValue = (configState, defaultValue) => {
849
850
  const requestedType = getRequestedType(defaultValue);
850
851
  if (!value) return {
851
852
  parsedValue: defaultValue,
853
+ parsedValueId: void 0,
852
854
  requestedType,
853
855
  usedDefault: true,
854
856
  reason: "value-missing"
855
857
  };
856
- if (configState.type === "json") return parseJson(value, defaultValue, requestedType);
858
+ if (configState.type === "json") return parseJson(value, defaultValue, requestedType, configState.valueId);
857
859
  if (typeof defaultValue === "string") return {
858
860
  parsedValue: value,
861
+ parsedValueId: configState.valueId,
859
862
  requestedType,
860
863
  usedDefault: false,
861
864
  reason: "found-match"
862
865
  };
863
- if (typeof defaultValue === "boolean" && configState.type === "boolean") {
866
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
864
867
  const boolValue = parseConfigBoolean(value);
865
868
  const hasBoolean = typeof boolValue === "boolean";
866
869
  return {
867
870
  parsedValue: hasBoolean ? boolValue : defaultValue,
871
+ parsedValueId: hasBoolean ? configState.valueId : void 0,
868
872
  requestedType,
869
873
  usedDefault: !hasBoolean,
870
874
  reason: hasBoolean ? "found-match" : "invalid-boolean"
@@ -875,16 +879,18 @@ const parseConfigValue = (configState, defaultValue) => {
875
879
  const hasNumber = typeof numValue === "number";
876
880
  return {
877
881
  parsedValue: hasNumber ? numValue : defaultValue,
882
+ parsedValueId: hasNumber ? configState.valueId : void 0,
878
883
  requestedType,
879
884
  usedDefault: !hasNumber,
880
885
  reason: hasNumber ? "found-match" : "invalid-number"
881
886
  };
882
887
  }
883
- if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum")) {
888
+ if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum" || configState.type === "string")) {
884
889
  const numValue = parseConfigFloat(value);
885
890
  const hasNumber = typeof numValue === "number";
886
891
  return {
887
892
  parsedValue: hasNumber ? numValue : defaultValue,
893
+ parsedValueId: hasNumber ? configState.valueId : void 0,
888
894
  requestedType,
889
895
  usedDefault: !hasNumber,
890
896
  reason: hasNumber ? "found-match" : "invalid-number"
@@ -892,6 +898,7 @@ const parseConfigValue = (configState, defaultValue) => {
892
898
  }
893
899
  return {
894
900
  parsedValue: value,
901
+ parsedValueId: configState.valueId,
895
902
  requestedType,
896
903
  usedDefault: false,
897
904
  reason: "found-match"
@@ -915,9 +922,10 @@ const parseConfigFloat = (value) => {
915
922
  if (isNaN(num) || !isFinite(num)) return;
916
923
  return num;
917
924
  };
918
- const parseJson = (jsonString, defaultValue, requestedType) => {
925
+ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
919
926
  if (typeof defaultValue === "string") return {
920
927
  parsedValue: jsonString,
928
+ parsedValueId: valueId,
921
929
  requestedType,
922
930
  usedDefault: false,
923
931
  reason: "found-match"
@@ -925,6 +933,7 @@ const parseJson = (jsonString, defaultValue, requestedType) => {
925
933
  try {
926
934
  return {
927
935
  parsedValue: JSON.parse(jsonString),
936
+ parsedValueId: valueId,
928
937
  requestedType,
929
938
  usedDefault: false,
930
939
  reason: "found-match"
@@ -932,6 +941,7 @@ const parseJson = (jsonString, defaultValue, requestedType) => {
932
941
  } catch {
933
942
  return {
934
943
  parsedValue: defaultValue,
944
+ parsedValueId: void 0,
935
945
  requestedType,
936
946
  usedDefault: true,
937
947
  reason: "invalid-json"
@@ -1007,25 +1017,35 @@ const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
1007
1017
  //#endregion
1008
1018
  //#region ../shared/src/telemetry/utils.ts
1009
1019
  const CONFIG_VALUE_MAX_LENGTH = 500;
1010
- const sanitizeValue = (value, type) => {
1011
- if (type === "json" || type == null && typeof value === "object") try {
1012
- return { digest: djb2Hash(JSON.stringify(value)) };
1013
- } catch {
1014
- return { digest: value.toString() };
1020
+ const compactTelemetryValue = async (tv, valueIdGenerator) => {
1021
+ if (tv.valueId) return { valueId: tv.valueId };
1022
+ if (tv.value) {
1023
+ if (tv.value.length <= CONFIG_VALUE_MAX_LENGTH && tv.type != "json") return { value: tv.value };
1024
+ return { valueId: await valueIdGenerator(tv.value) };
1015
1025
  }
1016
- if (value.toString().length > CONFIG_VALUE_MAX_LENGTH) return { digest: djb2Hash(value.toString()) };
1017
- else return { value: value.toString() };
1018
- };
1019
- const djb2Hash = (data) => {
1020
- return djb2(new TextEncoder().encode(data)).toString(16).padStart(8, "0");
1026
+ return tv;
1021
1027
  };
1022
- const djb2 = (bytes) => {
1023
- let hash = 5381;
1024
- for (let i = 0; i < bytes.length; i++) {
1025
- hash = (hash << 5) + hash + (bytes[i] ?? 0);
1026
- hash = hash >>> 0;
1028
+ const mapToTelemetryValue = (ev) => {
1029
+ if (ev.type === "json" || ev.type == null && typeof ev.value === "object") {
1030
+ if (ev.valueId) return {
1031
+ valueId: ev.valueId,
1032
+ type: "json"
1033
+ };
1034
+ try {
1035
+ return {
1036
+ value: JSON.stringify(ev.value),
1037
+ type: "json"
1038
+ };
1039
+ } catch {
1040
+ return {
1041
+ value: ev.value.toString(),
1042
+ type: "json"
1043
+ };
1044
+ }
1027
1045
  }
1028
- return hash;
1046
+ const stringValue = ev.value?.toString();
1047
+ if (stringValue && stringValue.length <= CONFIG_VALUE_MAX_LENGTH) return { value: stringValue };
1048
+ return ev.valueId ? { valueId: ev.valueId } : { value: stringValue };
1029
1049
  };
1030
1050
  const isEventListEmpty = (eventList) => {
1031
1051
  const keys = Object.keys(eventList);
@@ -1189,9 +1209,6 @@ var TelemetryEventCollector = class {
1189
1209
  const initialDelay = options.initialFlushIntervalDelay ?? 5e3;
1190
1210
  this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), initialDelay);
1191
1211
  }
1192
- sanitizeValue(value, type) {
1193
- return sanitizeValue(value, type);
1194
- }
1195
1212
  async flushAndScheduleNext() {
1196
1213
  if ((await this.flush()).fatalError) {
1197
1214
  this.collectEvents = false;
@@ -1199,19 +1216,6 @@ var TelemetryEventCollector = class {
1199
1216
  this.logger.warn("[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.");
1200
1217
  } else this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
1201
1218
  }
1202
- async flush() {
1203
- if (!this.reporter) return {
1204
- success: false,
1205
- fatalError: false
1206
- };
1207
- const evaluationSnapshot = this.evaluationEventQueue.takeSnapshot();
1208
- return await this.reporter?.report({
1209
- context: this._context,
1210
- discreteEvents: {},
1211
- aggregatedEvents: { evaluatedConfig: this.aggregator.aggregate(evaluationSnapshot) },
1212
- droppedEvents: { evaluatedConfig: evaluationSnapshot.droppedCount }
1213
- });
1214
- }
1215
1219
  cancelScheduledFlush() {
1216
1220
  clearTimeout(this.flushTimeout);
1217
1221
  }
@@ -1258,9 +1262,31 @@ var LimitedMap = class extends Map {
1258
1262
  }
1259
1263
  };
1260
1264
  //#endregion
1265
+ //#region ../shared/src/telemetry/EvaluatedConfigEventPreprocessor.ts
1266
+ var EvaluatedConfigEventPreprocessor = class {
1267
+ constructor(valueIdGenerator) {
1268
+ this.valueIdGenerator = valueIdGenerator;
1269
+ this.valueIdGenerator = valueIdGenerator;
1270
+ }
1271
+ async process(snapshot) {
1272
+ const events = await Promise.all(snapshot.events.map(async (e) => {
1273
+ return {
1274
+ ...e,
1275
+ defaultValue: await compactTelemetryValue(e.defaultValue, this.valueIdGenerator),
1276
+ evaluatedValue: await compactTelemetryValue(e.evaluatedValue, this.valueIdGenerator)
1277
+ };
1278
+ }));
1279
+ return {
1280
+ ...snapshot,
1281
+ events
1282
+ };
1283
+ }
1284
+ };
1285
+ //#endregion
1261
1286
  //#region ../js-server-sdk/src/telemetry/ServerTelemetryEventCollector.ts
1262
1287
  const DEFAULT_CONTEXTS_LIMIT = 1e3;
1263
1288
  var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
1289
+ evaluationEventSnapshotPreprocessor;
1264
1290
  reporter;
1265
1291
  _context;
1266
1292
  contexts;
@@ -1268,6 +1294,7 @@ var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
1268
1294
  super(options);
1269
1295
  this.reporter = new ServerEventReporter(options);
1270
1296
  this.contexts = new LimitedMap(options.contextLimit ?? DEFAULT_CONTEXTS_LIMIT);
1297
+ this.evaluationEventSnapshotPreprocessor = new EvaluatedConfigEventPreprocessor(options.valueIdGenerator);
1271
1298
  }
1272
1299
  evaluatedConfig(payload) {
1273
1300
  if (!this.collectEvents) return;
@@ -1281,8 +1308,15 @@ var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
1281
1308
  sanitizeEvaluatedConfigEvent(event) {
1282
1309
  return {
1283
1310
  ...event,
1284
- defaultValue: this.sanitizeValue(event.defaultValue, event.type),
1285
- evaluatedValue: this.sanitizeValue(event.evaluatedValue, event.type)
1311
+ defaultValue: mapToTelemetryValue({
1312
+ value: event.defaultValue,
1313
+ type: event.type
1314
+ }),
1315
+ evaluatedValue: mapToTelemetryValue({
1316
+ value: event.evaluatedValue,
1317
+ valueId: event.evaluatedValueId,
1318
+ type: event.type
1319
+ })
1286
1320
  };
1287
1321
  }
1288
1322
  snapshotContexts() {
@@ -1299,7 +1333,7 @@ var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
1299
1333
  success: false,
1300
1334
  fatalError: false
1301
1335
  };
1302
- const evaluationSnapshot = this.evaluationEventQueue.takeSnapshot();
1336
+ const evaluationSnapshot = await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());
1303
1337
  const contextsSnapshot = this.snapshotContexts();
1304
1338
  return await this.reporter?.report({
1305
1339
  context: this._context,
@@ -1313,6 +1347,27 @@ var ServerTelemetryEventCollector = class extends TelemetryEventCollector {
1313
1347
  }
1314
1348
  };
1315
1349
  //#endregion
1350
+ //#region ../shared/src/base62-encoder.ts
1351
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1352
+ const toBase62 = (buf, length) => {
1353
+ const ZERO = BigInt(0);
1354
+ const BASE = BigInt(62);
1355
+ const hex = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
1356
+ let n = BigInt("0x" + hex);
1357
+ let result = "";
1358
+ while (n > ZERO) {
1359
+ result = BASE62[Number(n % BASE)] + result;
1360
+ n /= BASE;
1361
+ }
1362
+ return result.padStart(length, "0");
1363
+ };
1364
+ const BASE62_LENGTH = Math.ceil(128 / Math.log2(62));
1365
+ //#endregion
1366
+ //#region ../js-server-sdk/src/telemetry/value-id-generator.ts
1367
+ const generateValueId = async (v) => {
1368
+ return toBase62(createHash("sha256").update(v?.toString() ?? "").digest().subarray(0, 16), BASE62_LENGTH);
1369
+ };
1370
+ //#endregion
1316
1371
  //#region ../js-server-sdk/src/DefaultConfigDirectorClient.ts
1317
1372
  const defaultBaseUrl = new URL("https://server-sdk-api.configdirector.com");
1318
1373
  const DEFAULT_FLUSH_INTERVAL = 3e4;
@@ -1347,7 +1402,8 @@ var DefaultConfigDirectorClient = class {
1347
1402
  flushIntervalDelay: clientOptions?.telemetry?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
1348
1403
  evaluationQueueLimit: Math.ceil(queueLimit * .7),
1349
1404
  contextLimit: Math.floor(queueLimit * .3),
1350
- urlFactory: defaultUrlFactory
1405
+ urlFactory: defaultUrlFactory,
1406
+ valueIdGenerator: generateValueId
1351
1407
  });
1352
1408
  this.transport = new transportConstructor({
1353
1409
  serverSdkKey,
@@ -1377,6 +1433,14 @@ var DefaultConfigDirectorClient = class {
1377
1433
  }
1378
1434
  this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${configKeys.length} key(s)`, { keys: configKeys });
1379
1435
  });
1436
+ this.registerHandler("clientReady", clientOptions?.hooks?.clientReady);
1437
+ this.registerHandler("configEvaluated", clientOptions?.hooks?.configEvaluated);
1438
+ this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
1439
+ }
1440
+ registerHandler(event, handler) {
1441
+ if (!handler) return;
1442
+ const handlers = Array.isArray(handler) ? handler : [handler];
1443
+ for (const h of handlers) if (typeof h === "function") this.on(event, h);
1380
1444
  }
1381
1445
  async initialize() {
1382
1446
  try {
@@ -1460,14 +1524,24 @@ var DefaultConfigDirectorClient = class {
1460
1524
  defaultValue,
1461
1525
  requestedType: parseResult.requestedType,
1462
1526
  evaluatedValue: parseResult.parsedValue,
1527
+ evaluatedValueId: parseResult.parsedValueId,
1463
1528
  usedDefault: parseResult.usedDefault,
1464
1529
  evaluationReason: parseResult.reason
1465
1530
  }
1466
1531
  });
1532
+ this.dispatchEvaluationEvent({
1533
+ key: configKey,
1534
+ value: parseResult.parsedValue,
1535
+ valueId: parseResult.parsedValueId ?? void 0,
1536
+ isDefaultValue: parseResult.usedDefault,
1537
+ reason: parseResult.reason,
1538
+ context
1539
+ });
1467
1540
  this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
1468
1541
  return parseResult.parsedValue;
1469
1542
  } else {
1470
1543
  this.logger.debug(`[ConfigDirectorClient] No config state found for '${configKey}', returning default value '${defaultValue}'`);
1544
+ const reason = this.isReady ? "config-state-missing" : "client-not-ready";
1471
1545
  this.usageEventCollector.evaluatedConfig({
1472
1546
  context,
1473
1547
  evaluation: {
@@ -1476,12 +1550,22 @@ var DefaultConfigDirectorClient = class {
1476
1550
  requestedType: getRequestedType(defaultValue),
1477
1551
  evaluatedValue: defaultValue,
1478
1552
  usedDefault: true,
1479
- evaluationReason: "config-state-missing"
1553
+ evaluationReason: reason
1480
1554
  }
1481
1555
  });
1556
+ this.dispatchEvaluationEvent({
1557
+ key: configKey,
1558
+ value: defaultValue,
1559
+ isDefaultValue: true,
1560
+ reason,
1561
+ context
1562
+ });
1482
1563
  return defaultValue;
1483
1564
  }
1484
1565
  }
1566
+ dispatchEvaluationEvent(event) {
1567
+ setTimeout(() => this.eventEmitter.emit("configEvaluated", { evaluation: event }), 0);
1568
+ }
1485
1569
  parseUrl(url) {
1486
1570
  if (!url) return;
1487
1571
  try {
@@ -1552,7 +1636,7 @@ var plugin_default = defineNitroPlugin((nitroApp) => {
1552
1636
  const { serverSdkKey, baseUrl } = runtimeConfig.configdirector;
1553
1637
  const client = new DefaultConfigDirectorClient(serverSdkKey, {
1554
1638
  sdkName: "nuxt-sdk",
1555
- sdkVersion: "0.7.0"
1639
+ sdkVersion: "0.9.0"
1556
1640
  }, {
1557
1641
  logger,
1558
1642
  ...baseUrl ? { connection: { url: baseUrl } } : {}
@@ -502,22 +502,25 @@ const parseConfigValue = (configState, defaultValue) => {
502
502
  const requestedType = getRequestedType(defaultValue);
503
503
  if (!value) return {
504
504
  parsedValue: defaultValue,
505
+ parsedValueId: void 0,
505
506
  requestedType,
506
507
  usedDefault: true,
507
508
  reason: "value-missing"
508
509
  };
509
- if (configState.type === "json") return parseJson(value, defaultValue, requestedType);
510
+ if (configState.type === "json") return parseJson(value, defaultValue, requestedType, configState.valueId);
510
511
  if (typeof defaultValue === "string") return {
511
512
  parsedValue: value,
513
+ parsedValueId: configState.valueId,
512
514
  requestedType,
513
515
  usedDefault: false,
514
516
  reason: "found-match"
515
517
  };
516
- if (typeof defaultValue === "boolean" && configState.type === "boolean") {
518
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
517
519
  const boolValue = parseConfigBoolean(value);
518
520
  const hasBoolean = typeof boolValue === "boolean";
519
521
  return {
520
522
  parsedValue: hasBoolean ? boolValue : defaultValue,
523
+ parsedValueId: hasBoolean ? configState.valueId : void 0,
521
524
  requestedType,
522
525
  usedDefault: !hasBoolean,
523
526
  reason: hasBoolean ? "found-match" : "invalid-boolean"
@@ -528,16 +531,18 @@ const parseConfigValue = (configState, defaultValue) => {
528
531
  const hasNumber = typeof numValue === "number";
529
532
  return {
530
533
  parsedValue: hasNumber ? numValue : defaultValue,
534
+ parsedValueId: hasNumber ? configState.valueId : void 0,
531
535
  requestedType,
532
536
  usedDefault: !hasNumber,
533
537
  reason: hasNumber ? "found-match" : "invalid-number"
534
538
  };
535
539
  }
536
- if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum")) {
540
+ if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum" || configState.type === "string")) {
537
541
  const numValue = parseConfigFloat(value);
538
542
  const hasNumber = typeof numValue === "number";
539
543
  return {
540
544
  parsedValue: hasNumber ? numValue : defaultValue,
545
+ parsedValueId: hasNumber ? configState.valueId : void 0,
541
546
  requestedType,
542
547
  usedDefault: !hasNumber,
543
548
  reason: hasNumber ? "found-match" : "invalid-number"
@@ -545,6 +550,7 @@ const parseConfigValue = (configState, defaultValue) => {
545
550
  }
546
551
  return {
547
552
  parsedValue: value,
553
+ parsedValueId: configState.valueId,
548
554
  requestedType,
549
555
  usedDefault: false,
550
556
  reason: "found-match"
@@ -568,9 +574,10 @@ const parseConfigFloat = (value) => {
568
574
  if (isNaN(num) || !isFinite(num)) return;
569
575
  return num;
570
576
  };
571
- const parseJson = (jsonString, defaultValue, requestedType) => {
577
+ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
572
578
  if (typeof defaultValue === "string") return {
573
579
  parsedValue: jsonString,
580
+ parsedValueId: valueId,
574
581
  requestedType,
575
582
  usedDefault: false,
576
583
  reason: "found-match"
@@ -578,6 +585,7 @@ const parseJson = (jsonString, defaultValue, requestedType) => {
578
585
  try {
579
586
  return {
580
587
  parsedValue: JSON.parse(jsonString),
588
+ parsedValueId: valueId,
581
589
  requestedType,
582
590
  usedDefault: false,
583
591
  reason: "found-match"
@@ -585,6 +593,7 @@ const parseJson = (jsonString, defaultValue, requestedType) => {
585
593
  } catch {
586
594
  return {
587
595
  parsedValue: defaultValue,
596
+ parsedValueId: void 0,
588
597
  requestedType,
589
598
  usedDefault: true,
590
599
  reason: "invalid-json"
@@ -859,18 +868,27 @@ var DefaultConfigDirectorClient = class {
859
868
  const configKeys = Object.keys(configSet.configs);
860
869
  if (!this.configSet || configSet.kind == "full") {
861
870
  this.configSet = configSet;
862
- this.eventEmitter.emit("configsUpdated", { keys: configKeys });
871
+ this.emit("configsUpdated", { keys: configKeys });
863
872
  this.updateWatchers(configSet.configs);
864
873
  } else {
865
874
  this.configSet.configs = {
866
875
  ...this.configSet.configs,
867
876
  ...configSet.configs
868
877
  };
869
- this.eventEmitter.emit("configsUpdated", { keys: configKeys });
878
+ this.emit("configsUpdated", { keys: configKeys });
870
879
  this.updateWatchers(configSet.configs);
871
880
  }
872
881
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
873
882
  });
883
+ this.registerHandler("clientReady", clientOptions?.hooks?.clientReady);
884
+ this.registerHandler("configEvaluated", clientOptions?.hooks?.configEvaluated);
885
+ this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
886
+ this.registerHandler("contextUpdated", clientOptions?.hooks?.contextUpdated);
887
+ }
888
+ registerHandler(event, handler) {
889
+ if (!handler) return;
890
+ const handlers = Array.isArray(handler) ? handler : [handler];
891
+ for (const h of handlers) if (typeof h === "function") this.on(event, h);
874
892
  }
875
893
  getTransportConstructor(connectionMode) {
876
894
  switch (connectionMode) {
@@ -897,14 +915,14 @@ var DefaultConfigDirectorClient = class {
897
915
  }).then(() => {
898
916
  this.ready = true;
899
917
  this.initializing = false;
900
- this.eventEmitter.emit("clientReady", { action: caller });
918
+ this.emit("clientReady", { action: caller });
901
919
  this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
902
920
  });
903
921
  const startTime = (/* @__PURE__ */ new Date()).getTime();
904
922
  await this.transport.connect(context ?? {}, this.timeout);
905
923
  this.currentContext = context;
906
924
  this.telemetryClient.updateContext(context);
907
- this.eventEmitter.emit("contextUpdated", { context });
925
+ this.emit("contextUpdated", { context });
908
926
  const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
909
927
  const remainingTimeout = this.timeout - elapsedTime;
910
928
  clearTimeout(this.timeoutTimer);
@@ -954,6 +972,7 @@ var DefaultConfigDirectorClient = class {
954
972
  getValueFromConfigState(configKey, configState, defaultValue) {
955
973
  if (!configState) {
956
974
  this.logger.debug(`[ConfigDirectorClient] No config state found for '${configKey}', returning default value '${defaultValue}'`);
975
+ const reason = this.isReady ? "config-state-missing" : "client-not-ready";
957
976
  this.telemetryClient.evaluatedConfig({
958
977
  contextId: this.currentContext?.id,
959
978
  key: configKey,
@@ -961,7 +980,14 @@ var DefaultConfigDirectorClient = class {
961
980
  requestedType: getRequestedType(defaultValue),
962
981
  evaluatedValue: defaultValue,
963
982
  usedDefault: true,
964
- evaluationReason: "config-state-missing"
983
+ evaluationReason: reason
984
+ });
985
+ this.dispatchEvaluationEvent({
986
+ key: configKey,
987
+ value: defaultValue,
988
+ isDefaultValue: true,
989
+ reason,
990
+ context: this.context
965
991
  });
966
992
  return defaultValue;
967
993
  }
@@ -973,12 +999,31 @@ var DefaultConfigDirectorClient = class {
973
999
  defaultValue,
974
1000
  requestedType: parseResult.requestedType,
975
1001
  evaluatedValue: parseResult.parsedValue,
1002
+ evaluatedValueId: parseResult.parsedValueId,
976
1003
  usedDefault: parseResult.usedDefault,
977
1004
  evaluationReason: parseResult.reason
978
1005
  });
1006
+ this.dispatchEvaluationEvent({
1007
+ key: configKey,
1008
+ value: parseResult.parsedValue,
1009
+ valueId: parseResult.parsedValueId ?? void 0,
1010
+ isDefaultValue: parseResult.usedDefault,
1011
+ reason: parseResult.reason,
1012
+ context: this.context
1013
+ });
979
1014
  this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
980
1015
  return parseResult.parsedValue;
981
1016
  }
1017
+ dispatchEvaluationEvent(event) {
1018
+ setTimeout(() => this.emit("configEvaluated", { evaluation: event }), 0);
1019
+ }
1020
+ emit(name, payload) {
1021
+ try {
1022
+ this.eventEmitter.emit(name, payload);
1023
+ } catch (error) {
1024
+ this.logger.error(`[ConfigDirectorClient] Error executing event handlers for '${name}'`, error);
1025
+ }
1026
+ }
982
1027
  parseUrl(url, urlFactory) {
983
1028
  if (!url) return;
984
1029
  try {
@@ -1037,25 +1082,27 @@ var DefaultConfigDirectorClient = class {
1037
1082
  //#endregion
1038
1083
  //#region ../shared/src/telemetry/utils.ts
1039
1084
  const CONFIG_VALUE_MAX_LENGTH = 500;
1040
- const sanitizeValue = (value, type) => {
1041
- if (type === "json" || type == null && typeof value === "object") try {
1042
- return { digest: djb2Hash(JSON.stringify(value)) };
1043
- } catch {
1044
- return { digest: value.toString() };
1045
- }
1046
- if (value.toString().length > CONFIG_VALUE_MAX_LENGTH) return { digest: djb2Hash(value.toString()) };
1047
- else return { value: value.toString() };
1048
- };
1049
- const djb2Hash = (data) => {
1050
- return djb2(new TextEncoder().encode(data)).toString(16).padStart(8, "0");
1051
- };
1052
- const djb2 = (bytes) => {
1053
- let hash = 5381;
1054
- for (let i = 0; i < bytes.length; i++) {
1055
- hash = (hash << 5) + hash + (bytes[i] ?? 0);
1056
- hash = hash >>> 0;
1085
+ const mapToTelemetryValue = (ev) => {
1086
+ if (ev.type === "json" || ev.type == null && typeof ev.value === "object") {
1087
+ if (ev.valueId) return {
1088
+ valueId: ev.valueId,
1089
+ type: "json"
1090
+ };
1091
+ try {
1092
+ return {
1093
+ value: JSON.stringify(ev.value),
1094
+ type: "json"
1095
+ };
1096
+ } catch {
1097
+ return {
1098
+ value: ev.value.toString(),
1099
+ type: "json"
1100
+ };
1101
+ }
1057
1102
  }
1058
- return hash;
1103
+ const stringValue = ev.value?.toString();
1104
+ if (stringValue && stringValue.length <= CONFIG_VALUE_MAX_LENGTH) return { value: stringValue };
1105
+ return ev.valueId ? { valueId: ev.valueId } : { value: stringValue };
1059
1106
  };
1060
1107
  //#endregion
1061
1108
  //#region ../js-browser-client/src/telemetry/WebWorkerTelemetryClient.ts
@@ -1077,7 +1124,7 @@ var WebWorkerTelemetryClient = class {
1077
1124
  flushIntervalDelay: options.flushIntervalDelay ?? 3e4
1078
1125
  }
1079
1126
  };
1080
- this.worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=(e,n)=>{if(n===`json`||n==null&&typeof e==`object`)try{return{digest:t(JSON.stringify(e))}}catch{return{digest:e.toString()}}return e.toString().length>500?{digest:t(e.toString())}:{value:e.toString()}},t=e=>n(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),n=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+(e[n]??0),t>>>=0;return t},r=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},i=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},a=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},o=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var s=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return r(e.discreteEvents)&&r(e.aggregatedEvents)&&i(e.droppedEvents)}async sendReport(e){try{let t=await a(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=o(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},c=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},l=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},u=class{constructor(e){this.aggregator=new c,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new l(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(t,n){return e(t,n)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),await this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},d=class extends u{constructor(e){super(e),this.reporter=new s(e)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}};let f=(e,t)=>new URL(e,t?.toString()),p,m=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),h=e=>{try{self.postMessage(e)}catch{self.postMessage(m(e.payload.level,e.payload.message))}},g={debug:function(e,...t){h(m(`debug`,e,t))},info:function(e,...t){h(m(`info`,e,t))},warn:function(e,...t){h(m(`warn`,e,t))},error:function(e,...t){h(m(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!p){g.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(g.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:p=new d({...t.payload,baseUrl:f(t.payload.baseUrl),urlFactory:f,logger:g});break;case`EvaluatedConfigEvent`:p?.evaluatedConfig(t.payload);break;case`UpdateContext`:p?.updateContext(t.payload.context);break;case`Flush`:p?.forceFlush();break;case`Close`:(p?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1127
+ this.worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=async(e,t)=>e.valueId?{valueId:e.valueId}:e.value?e.value.length<=500&&e.type!=`json`?{value:e.value}:{valueId:await t(e.value)}:e,t=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},n=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},r=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},i=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var a=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return t(e.discreteEvents)&&t(e.aggregatedEvents)&&n(e.droppedEvents)}async sendReport(e){try{let t=await r(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=i(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},o=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},s=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},c=class{constructor(e){this.aggregator=new o,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new s(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),await this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},l=class{constructor(e){this.valueIdGenerator=e,this.valueIdGenerator=e}async process(t){let n=await Promise.all(t.events.map(async t=>({...t,defaultValue:await e(t.defaultValue,this.valueIdGenerator),evaluatedValue:await e(t.evaluatedValue,this.valueIdGenerator)})));return{...t,events:n}}},u=class extends c{constructor(e){super(e),this.reporter=new a(e),this.evaluationEventSnapshotPreprocessor=new l(e.valueIdGenerator)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}};let d=(e,t)=>new URL(e,t?.toString()),f=(e,t)=>{let n=BigInt(0),r=BigInt(62),i=Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),a=BigInt(`0x`+i),o=``;for(;a>n;)o=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`[Number(a%r)]+o,a/=r;return o.padStart(t,`0`)},p=Math.ceil(128/Math.log2(62)),m=new TextEncoder,h=async e=>{let t=await crypto.subtle.digest(`SHA-256`,m.encode(e?.toString()??``));return f(new Uint8Array(t,0,16),p)},g,_=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),v=e=>{try{self.postMessage(e)}catch{self.postMessage(_(e.payload.level,e.payload.message))}},y={debug:function(e,...t){v(_(`debug`,e,t))},info:function(e,...t){v(_(`info`,e,t))},warn:function(e,...t){v(_(`warn`,e,t))},error:function(e,...t){v(_(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!g){y.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(y.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:g=new u({...t.payload,baseUrl:d(t.payload.baseUrl),urlFactory:d,valueIdGenerator:h,logger:y});break;case`EvaluatedConfigEvent`:g?.evaluatedConfig(t.payload);break;case`UpdateContext`:g?.updateContext(t.payload.context);break;case`Flush`:g?.forceFlush();break;case`Close`:(g?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1081
1128
  this.worker.onmessage = (message) => {
1082
1129
  this.handleWorkerEvent(message.data);
1083
1130
  };
@@ -1108,8 +1155,15 @@ var WebWorkerTelemetryClient = class {
1108
1155
  sanitizeEvaluatedConfigEvent(event) {
1109
1156
  return {
1110
1157
  ...event,
1111
- defaultValue: sanitizeValue(event.defaultValue, event.type),
1112
- evaluatedValue: sanitizeValue(event.evaluatedValue, event.type)
1158
+ defaultValue: mapToTelemetryValue({
1159
+ value: event.defaultValue,
1160
+ type: event.type
1161
+ }),
1162
+ evaluatedValue: mapToTelemetryValue({
1163
+ value: event.evaluatedValue,
1164
+ valueId: event.evaluatedValueId,
1165
+ type: event.type
1166
+ })
1113
1167
  };
1114
1168
  }
1115
1169
  handleWorkerEvent(event) {
@@ -1151,6 +1205,29 @@ var WebWorkerTelemetryClient = class {
1151
1205
  }
1152
1206
  };
1153
1207
  //#endregion
1208
+ //#region ../shared/src/base62-encoder.ts
1209
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1210
+ const toBase62 = (buf, length) => {
1211
+ const ZERO = BigInt(0);
1212
+ const BASE = BigInt(62);
1213
+ const hex = Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
1214
+ let n = BigInt("0x" + hex);
1215
+ let result = "";
1216
+ while (n > ZERO) {
1217
+ result = BASE62[Number(n % BASE)] + result;
1218
+ n /= BASE;
1219
+ }
1220
+ return result.padStart(length, "0");
1221
+ };
1222
+ const BASE62_LENGTH = Math.ceil(128 / Math.log2(62));
1223
+ //#endregion
1224
+ //#region ../js-browser-client/src/telemetry/value-id-generator.ts
1225
+ const encoder = new TextEncoder();
1226
+ const generateValueId = async (v) => {
1227
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(v?.toString() ?? ""));
1228
+ return toBase62(new Uint8Array(hashBuffer, 0, 16), BASE62_LENGTH);
1229
+ };
1230
+ //#endregion
1154
1231
  //#region ../js-browser-client/src/BrowserConfigDirectorClient.ts
1155
1232
  const createBrowserClient = (clientSdkKey, sdkOptions, clientOptions) => {
1156
1233
  const logger = clientOptions?.logger ?? createDefaultLogger$1();
@@ -1158,7 +1235,8 @@ const createBrowserClient = (clientSdkKey, sdkOptions, clientOptions) => {
1158
1235
  sdkKey: clientSdkKey,
1159
1236
  logger,
1160
1237
  baseUrl: parseUrl(clientOptions?.connection?.url) ?? "https://client-sdk-api.configdirector.com",
1161
- urlFactory: defaultUrlFactory
1238
+ urlFactory: defaultUrlFactory,
1239
+ valueIdGenerator: generateValueId
1162
1240
  }), clientSdkKey, sdkOptions, {
1163
1241
  ...clientOptions,
1164
1242
  logger
@@ -1217,7 +1295,7 @@ var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1217
1295
  const { clientSdkKey, appName, appVersion, baseUrl } = runtimeConfig.public.configdirector;
1218
1296
  const client = createBrowserClient(clientSdkKey, {
1219
1297
  sdkName: "nuxt-sdk",
1220
- sdkVersion: "0.7.0"
1298
+ sdkVersion: "0.9.0"
1221
1299
  }, {
1222
1300
  metadata: {
1223
1301
  appName,
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-BDwzL0dp.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-CzmBIfqo.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => ConfigDirectorClient;
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-BDwzL0dp.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-CzmBIfqo.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => ConfigDirectorClient;
@@ -0,0 +1,6 @@
1
+ import { t as ClientHooks } from "../../types-CzmBIfqo.mjs";
2
+
3
+ //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
+ declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
5
+ //#endregion
6
+ export { useConfigDirectorServerHooks };
@@ -0,0 +1,6 @@
1
+ import { t as ClientHooks } from "../../types-CzmBIfqo.mjs";
2
+
3
+ //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
+ declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
5
+ //#endregion
6
+ export { useConfigDirectorServerHooks };
@@ -0,0 +1,14 @@
1
+ import { useNitroApp } from "nitropack/runtime";
2
+ //#region src/runtime/server/composables/useConfigDirectorServerHooks.ts
3
+ const useConfigDirectorServerHooks = (hooks) => {
4
+ const client = useNitroApp().configDirectorClient;
5
+ if (!client) throw new Error("The ConfigDirector Nitro plugin was not initialized");
6
+ const entries = Object.entries(hooks);
7
+ for (const [event, handler] of entries) {
8
+ if (!handler) continue;
9
+ const handlers = Array.isArray(handler) ? handler : [handler];
10
+ for (const h of handlers) client.on(event, h);
11
+ }
12
+ };
13
+ //#endregion
14
+ export { useConfigDirectorServerHooks };
@@ -1,4 +1,4 @@
1
- import { r as ConfigValueType, t as ConfigDirectorContext } from "./types-f2z6DgES.mjs";
1
+ import { i as ConfigValueType, n as ConfigEvaluation, t as ConfigDirectorContext } from "./types-CHmsCChN.mjs";
2
2
 
3
3
  //#region ../js-client-core/src/Emitter.d.ts
4
4
  type EventType = string | symbol;
@@ -11,7 +11,14 @@ interface EventProvider<TEventsRecord extends EventsRecord> {
11
11
  }
12
12
  //#endregion
13
13
  //#region ../js-client-core/src/types.d.ts
14
+ type ClientHooks = {
15
+ clientReady?: HookHandler<"clientReady"> | HookHandler<"clientReady">[];
16
+ configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
17
+ contextUpdated?: HookHandler<"contextUpdated"> | HookHandler<"contextUpdated">[];
18
+ configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
19
+ };
14
20
  type ClientConnectAction = "initialization" | "context update" | "network resume";
21
+ type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
15
22
  type ClientEvents = {
16
23
  configsUpdated: {
17
24
  keys: string[];
@@ -22,6 +29,9 @@ type ClientEvents = {
22
29
  contextUpdated: {
23
30
  context: ConfigDirectorContext | undefined;
24
31
  };
32
+ configEvaluated: {
33
+ evaluation: ConfigEvaluation;
34
+ };
25
35
  };
26
36
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
27
37
  /**
@@ -132,4 +142,4 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
132
142
  dispose(): void;
133
143
  }
134
144
  //#endregion
135
- export { ConfigDirectorClient as t };
145
+ export { ConfigDirectorClient as n, ClientHooks as t };
@@ -10,6 +10,16 @@ type ConfigState = {
10
10
  key: string;
11
11
  type: ConfigType;
12
12
  value: string | undefined | null;
13
+ valueId?: string | undefined | null;
14
+ };
15
+ type EvaluationReason = "found-match" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean";
16
+ type ConfigEvaluation = {
17
+ key: string;
18
+ value: ConfigValueType;
19
+ valueId?: string;
20
+ isDefaultValue: boolean;
21
+ reason: EvaluationReason;
22
+ context?: ConfigDirectorContext;
13
23
  };
14
24
  /**
15
25
  * The user's context to be sent to ConfigDirector. This context will be used for targeting
@@ -47,4 +57,4 @@ type ConfigDirectorContext = {
47
57
  anonymous?: boolean;
48
58
  };
49
59
  //#endregion
50
- export { ConfigState as n, ConfigValueType as r, ConfigDirectorContext as t };
60
+ export { ConfigValueType as i, ConfigEvaluation as n, ConfigState as r, ConfigDirectorContext as t };
@@ -1,4 +1,4 @@
1
- import { n as ConfigState, r as ConfigValueType, t as ConfigDirectorContext } from "./types-f2z6DgES.mjs";
1
+ import { i as ConfigValueType, n as ConfigEvaluation, r as ConfigState, t as ConfigDirectorContext } from "./types-CHmsCChN.mjs";
2
2
 
3
3
  //#region ../js-server-sdk/src/types.d.ts
4
4
  type ClientEvents = {
@@ -6,7 +6,16 @@ type ClientEvents = {
6
6
  keys: string[];
7
7
  };
8
8
  clientReady: undefined;
9
+ configEvaluated: {
10
+ evaluation: ConfigEvaluation;
11
+ };
12
+ };
13
+ type ClientHooks = {
14
+ clientReady?: HookHandler<"clientReady"> | HookHandler<"clientReady">[];
15
+ configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
16
+ configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
9
17
  };
18
+ type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
10
19
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
11
20
  /**
12
21
  * The ConfigDirector SDK client object.
@@ -93,4 +102,4 @@ interface ConfigDirectorClient {
93
102
  dispose(): void;
94
103
  }
95
104
  //#endregion
96
- export { ConfigDirectorClient as t };
105
+ export { ConfigDirectorClient as n, ClientHooks as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/nuxt-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Nuxt SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",