@copilotkit/shared 1.69.2 → 1.70.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 (75) hide show
  1. package/dist/index.cjs +33 -13
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +20 -17
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +20 -17
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +30 -15
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.umd.js +170 -31
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/package.cjs +1 -1
  12. package/dist/package.mjs +1 -1
  13. package/dist/telemetry/index.d.mts +3 -2
  14. package/dist/telemetry/lambda-client.cjs +25 -4
  15. package/dist/telemetry/lambda-client.cjs.map +1 -1
  16. package/dist/telemetry/lambda-client.d.cts +14 -1
  17. package/dist/telemetry/lambda-client.d.cts.map +1 -1
  18. package/dist/telemetry/lambda-client.d.mts +14 -1
  19. package/dist/telemetry/lambda-client.d.mts.map +1 -1
  20. package/dist/telemetry/lambda-client.mjs +25 -5
  21. package/dist/telemetry/lambda-client.mjs.map +1 -1
  22. package/dist/telemetry/sampling.cjs +28 -0
  23. package/dist/telemetry/sampling.cjs.map +1 -0
  24. package/dist/telemetry/sampling.d.cts +37 -0
  25. package/dist/telemetry/sampling.d.cts.map +1 -0
  26. package/dist/telemetry/sampling.d.mts +37 -0
  27. package/dist/telemetry/sampling.d.mts.map +1 -0
  28. package/dist/telemetry/sampling.mjs +25 -0
  29. package/dist/telemetry/sampling.mjs.map +1 -0
  30. package/dist/telemetry/telemetry-client.cjs +72 -11
  31. package/dist/telemetry/telemetry-client.cjs.map +1 -1
  32. package/dist/telemetry/telemetry-client.d.cts +43 -1
  33. package/dist/telemetry/telemetry-client.d.cts.map +1 -1
  34. package/dist/telemetry/telemetry-client.d.mts +43 -1
  35. package/dist/telemetry/telemetry-client.d.mts.map +1 -1
  36. package/dist/telemetry/telemetry-client.mjs +73 -12
  37. package/dist/telemetry/telemetry-client.mjs.map +1 -1
  38. package/dist/utils/index.cjs +1 -0
  39. package/dist/utils/index.cjs.map +1 -1
  40. package/dist/utils/index.d.cts +2 -1
  41. package/dist/utils/index.d.cts.map +1 -1
  42. package/dist/utils/index.d.mts +2 -1
  43. package/dist/utils/index.d.mts.map +1 -1
  44. package/dist/utils/index.mjs +1 -0
  45. package/dist/utils/index.mjs.map +1 -1
  46. package/dist/utils/inspector-visibility.cjs +13 -0
  47. package/dist/utils/inspector-visibility.cjs.map +1 -0
  48. package/dist/utils/inspector-visibility.d.cts +18 -0
  49. package/dist/utils/inspector-visibility.d.cts.map +1 -0
  50. package/dist/utils/inspector-visibility.d.mts +18 -0
  51. package/dist/utils/inspector-visibility.d.mts.map +1 -0
  52. package/dist/utils/inspector-visibility.mjs +12 -0
  53. package/dist/utils/inspector-visibility.mjs.map +1 -0
  54. package/dist/utils/types.cjs.map +1 -1
  55. package/dist/utils/types.d.cts +46 -1
  56. package/dist/utils/types.d.cts.map +1 -1
  57. package/dist/utils/types.d.mts +46 -1
  58. package/dist/utils/types.d.mts.map +1 -1
  59. package/dist/utils/types.mjs.map +1 -1
  60. package/package.json +2 -2
  61. package/src/__tests__/license-context.test.ts +224 -25
  62. package/src/index.ts +72 -16
  63. package/src/telemetry/index.ts +2 -0
  64. package/src/telemetry/lambda-client.test.ts +336 -1
  65. package/src/telemetry/lambda-client.ts +56 -15
  66. package/src/telemetry/sampling.test.ts +65 -0
  67. package/src/telemetry/sampling.ts +70 -0
  68. package/src/telemetry/telemetry-blank-license-identity.test.ts +121 -0
  69. package/src/telemetry/telemetry-client.test.ts +438 -15
  70. package/src/telemetry/telemetry-client.ts +145 -30
  71. package/src/utils/__tests__/conditions.test.ts +161 -0
  72. package/src/utils/index.ts +1 -0
  73. package/src/utils/inspector-visibility.test.ts +43 -0
  74. package/src/utils/inspector-visibility.ts +17 -0
  75. package/src/utils/types.ts +52 -0
package/dist/package.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  //#region package.json
3
- var version = "1.69.2";
3
+ var version = "1.70.0";
4
4
 
5
5
  //#endregion
6
6
  Object.defineProperty(exports, 'version', {
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "1.69.2";
2
+ var version = "1.70.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1,2 +1,3 @@
1
- import { TelemetryClient, isTelemetryDisabled } from "./telemetry-client.mjs";
2
- import { LambdaSendOptions, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense } from "./lambda-client.mjs";
1
+ import { TelemetryCapture, TelemetryClient, TelemetryIdentity, isTelemetryDisabled } from "./telemetry-client.mjs";
2
+ import { SamplingMeta, TELEMETRY_EMITTER_V1, TELEMETRY_EMITTER_V2, TelemetryEmitter, TelemetryTransport, computeSamplingMeta } from "./sampling.mjs";
3
+ import { LambdaSendOptions, firstNonBlankTelemetryId, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense } from "./lambda-client.mjs";
@@ -2,6 +2,24 @@
2
2
  //#region src/telemetry/lambda-client.ts
3
3
  const TELEMETRY_SINK_URL = typeof process !== "undefined" && process.env?.COPILOTKIT_TELEMETRY_URL || "https://telemetry.copilotkit.ai/ingest";
4
4
  const FETCH_TIMEOUT_MS = 3e3;
5
+ const TELEMETRY_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
6
+ /**
7
+ * Return the first telemetry identity accepted by the ingest service.
8
+ *
9
+ * Empty and whitespace-only values are unconfigured placeholders and must not
10
+ * suppress a later identity source. Leading and trailing HTTP spaces and tabs
11
+ * are removed before validation. The ingest service accepts 1 to 128 ASCII
12
+ * letters, digits, underscores, and hyphens.
13
+ *
14
+ * @internal
15
+ */
16
+ function firstNonBlankTelemetryId(...candidates) {
17
+ for (const candidate of candidates) {
18
+ if (candidate === void 0) continue;
19
+ const normalized = candidate.replace(/^[\t ]+|[\t ]+$/g, "");
20
+ if (TELEMETRY_ID_PATTERN.test(normalized)) return normalized;
21
+ }
22
+ }
5
23
  const STRIPPED_KEYS = new Set(["cloud.public_api_key", "cloud.publicApiKey"]);
6
24
  function stripCloudKeys(obj) {
7
25
  if (!obj) return {};
@@ -14,12 +32,14 @@ function parseTelemetryIdFromLicense(token) {
14
32
  const parts = token.split(".");
15
33
  if (parts.length !== 3) return null;
16
34
  try {
17
- let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
35
+ const payload = parts[1];
36
+ if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) return null;
37
+ let b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
18
38
  const padding = (4 - b64.length % 4) % 4;
19
39
  b64 += "=".repeat(padding);
20
- const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("utf8");
40
+ const json = typeof Buffer !== "undefined" ? Buffer.from(b64, "base64").toString("utf8") : new TextDecoder().decode(Uint8Array.from(atob(b64), (character) => character.charCodeAt(0)));
21
41
  const decoded = JSON.parse(json);
22
- return typeof decoded.telemetry_id === "string" ? decoded.telemetry_id : null;
42
+ return firstNonBlankTelemetryId(typeof decoded.telemetry_id === "string" ? decoded.telemetry_id : void 0) ?? null;
23
43
  } catch {
24
44
  return null;
25
45
  }
@@ -41,7 +61,7 @@ async function send(opts) {
41
61
  },
42
62
  ts: Math.floor(Date.now() / 1e3)
43
63
  });
44
- const telemetryId = parseTelemetryIdFromLicense(opts.licenseToken);
64
+ const telemetryId = firstNonBlankTelemetryId(opts.telemetryId) ?? parseTelemetryIdFromLicense(opts.licenseToken);
45
65
  const headers = {
46
66
  "Content-Type": "application/json",
47
67
  "User-Agent": opts.packageName ? `CopilotKit-Runtime/${opts.packageVersion ?? "unknown"} (${opts.packageName})` : "CopilotKit-Runtime"
@@ -64,6 +84,7 @@ async function send(opts) {
64
84
  const lambdaClient = { send };
65
85
 
66
86
  //#endregion
87
+ exports.firstNonBlankTelemetryId = firstNonBlankTelemetryId;
67
88
  exports.lambdaClient = lambdaClient;
68
89
  exports.parseAndWarnTelemetryId = parseAndWarnTelemetryId;
69
90
  exports.parseTelemetryIdFromLicense = parseTelemetryIdFromLicense;
@@ -1 +1 @@
1
- {"version":3,"file":"lambda-client.cjs","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"sourcesContent":["// Telemetry sink client.\n//\n// Posts events to a CopilotKit-controlled telemetry-sink endpoint, which\n// fans out to Scarf, Reo, and any future destinations. Replaces the direct\n// per-vendor calls (scarf-client.ts) so that vendor changes don't require\n// SDK releases and so that downstream services we don't want exposed to\n// OSS readers (e.g. the email-enrichment service backing Reo) stay\n// private.\n//\n// Two attribution modes:\n// - Identified: a CopilotKit license token is configured. The token is\n// a JWT (header.payload.sig) whose payload carries `telemetry_id`.\n// The SDK base64url-decodes the payload — without verifying the\n// Ed25519 signature, which is the license-verifier's job and\n// emits the id via `X-CopilotKit-Telemetry-Id`. The Lambda uses it\n// to enrich events with the customer's email.\n// - Anonymous: no license token, or a malformed/non-JWT one. No\n// telemetry-id header; events still flow, attribution is best-effort\n// from request-level signals (IP, UA).\n//\n// Note: CopilotCloud customer API keys (`ck_<env>_<id>.<secret>`) are\n// unrelated to telemetry attribution. They flow into Segment / PostHog\n// via the v1 shared TelemetryClient and never reach this code path.\n//\n// Best-effort: every error is swallowed. Telemetry must not break the\n// host application.\n\nconst TELEMETRY_SINK_URL =\n (typeof process !== \"undefined\" && process.env?.COPILOTKIT_TELEMETRY_URL) ||\n \"https://telemetry.copilotkit.ai/ingest\";\n\nconst FETCH_TIMEOUT_MS = 3000;\n\nexport interface LambdaSendOptions {\n event: string;\n properties?: Record<string, unknown>;\n globalProperties?: Record<string, unknown>;\n packageName?: string;\n packageVersion?: string;\n // The CopilotKit license token (Ed25519-signed JWT), when one is\n // configured on the runtime. The sender base64url-decodes the payload\n // segment to extract `telemetry_id`; missing or malformed tokens\n // produce an anonymous send.\n licenseToken?: string;\n}\n\n// These fields aren't used by the telemetry service, so we strip them\n// at the wire boundary rather than rely on every caller to omit them.\n// Both the snake_case and camelCase variants are listed because callers\n// upstream use different conventions.\nconst STRIPPED_KEYS = new Set([\"cloud.public_api_key\", \"cloud.publicApiKey\"]);\n\nfunction stripCloudKeys(\n obj: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n if (!obj) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!STRIPPED_KEYS.has(k)) out[k] = v;\n }\n return out;\n}\n\n// Pull telemetry_id out of a CopilotKit license token without verifying\n// the signature. The token shape is a standard JWT\n// (`<header>.<payload>.<sig>`) with base64url-encoded segments; the\n// payload is JSON with a `telemetry_id` string field.\n//\n// Verification (Ed25519, key rotation, expiry) is the license-verifier\n// package's job. For telemetry attribution we only need the claimed id —\n// the trust model is claim-only on the Lambda side anyway.\n//\n// Exported so TelemetryClient setters can detect unparseable tokens at\n// configuration time and surface a single warning, instead of silently\n// emitting anonymous events on every capture.\nexport function parseTelemetryIdFromLicense(token?: string): string | null {\n if (!token) return null;\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n try {\n let b64 = parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = (4 - (b64.length % 4)) % 4;\n b64 += \"=\".repeat(padding);\n const json =\n typeof atob === \"function\"\n ? atob(b64)\n : Buffer.from(b64, \"base64\").toString(\"utf8\");\n const decoded = JSON.parse(json) as { telemetry_id?: unknown };\n return typeof decoded.telemetry_id === \"string\"\n ? decoded.telemetry_id\n : null;\n } catch {\n return null;\n }\n}\n\n// Parse the telemetry_id from a license token AND emit the rollout smoke\n// signal if the parse returned null. Returning the parsed id lets callers\n// cache it in one step (avoiding a second parseTelemetryIdFromLicense\n// pass) while keeping the warn text in lockstep between v1 (shared) and\n// v2 (runtime) TelemetryClient.setLicenseToken.\nexport function parseAndWarnTelemetryId(licenseToken: string): string | null {\n const telemetryId = parseTelemetryIdFromLicense(licenseToken);\n if (!telemetryId) {\n console.warn(\n \"[CopilotKit] License token did not yield a telemetry_id; telemetry events will be sent anonymously.\",\n );\n }\n return telemetryId;\n}\n\nexport async function send(opts: LambdaSendOptions): Promise<void> {\n try {\n const body = JSON.stringify({\n event: opts.event,\n properties: stripCloudKeys(opts.properties),\n global_properties: stripCloudKeys(opts.globalProperties),\n package: {\n name: opts.packageName,\n version: opts.packageVersion,\n },\n ts: Math.floor(Date.now() / 1000),\n });\n\n const telemetryId = parseTelemetryIdFromLicense(opts.licenseToken);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": opts.packageName\n ? `CopilotKit-Runtime/${opts.packageVersion ?? \"unknown\"} (${opts.packageName})`\n : \"CopilotKit-Runtime\",\n };\n if (telemetryId) {\n headers[\"X-CopilotKit-Telemetry-Id\"] = telemetryId;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n await fetch(TELEMETRY_SINK_URL, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timeoutId);\n }\n } catch {\n // Silent failure — telemetry must not break the application.\n }\n}\n\nexport const lambdaClient = { send };\n"],"mappings":";;AA2BA,MAAM,qBACH,OAAO,YAAY,eAAe,QAAQ,KAAK,4BAChD;AAEF,MAAM,mBAAmB;AAmBzB,MAAM,gBAAgB,IAAI,IAAI,CAAC,wBAAwB,qBAAqB,CAAC;AAE7E,SAAS,eACP,KACyB;AACzB,KAAI,CAAC,IAAK,QAAO,EAAE;CACnB,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CACtC,KAAI,CAAC,cAAc,IAAI,EAAE,CAAE,KAAI,KAAK;AAEtC,QAAO;;AAeT,SAAgB,4BAA4B,OAA+B;AACzE,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,KAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,KAAI;EACF,IAAI,MAAM,MAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI;EACxD,MAAM,WAAW,IAAK,IAAI,SAAS,KAAM;AACzC,SAAO,IAAI,OAAO,QAAQ;EAC1B,MAAM,OACJ,OAAO,SAAS,aACZ,KAAK,IAAI,GACT,OAAO,KAAK,KAAK,SAAS,CAAC,SAAS,OAAO;EACjD,MAAM,UAAU,KAAK,MAAM,KAAK;AAChC,SAAO,OAAO,QAAQ,iBAAiB,WACnC,QAAQ,eACR;SACE;AACN,SAAO;;;AASX,SAAgB,wBAAwB,cAAqC;CAC3E,MAAM,cAAc,4BAA4B,aAAa;AAC7D,KAAI,CAAC,YACH,SAAQ,KACN,sGACD;AAEH,QAAO;;AAGT,eAAsB,KAAK,MAAwC;AACjE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU;GAC1B,OAAO,KAAK;GACZ,YAAY,eAAe,KAAK,WAAW;GAC3C,mBAAmB,eAAe,KAAK,iBAAiB;GACxD,SAAS;IACP,MAAM,KAAK;IACX,SAAS,KAAK;IACf;GACD,IAAI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAClC,CAAC;EAEF,MAAM,cAAc,4BAA4B,KAAK,aAAa;EAClE,MAAM,UAAkC;GACtC,gBAAgB;GAChB,cAAc,KAAK,cACf,sBAAsB,KAAK,kBAAkB,UAAU,IAAI,KAAK,YAAY,KAC5E;GACL;AACD,MAAI,YACF,SAAQ,+BAA+B;EAGzC,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,iBAAiB;AACxE,MAAI;AACF,SAAM,MAAM,oBAAoB;IAC9B,QAAQ;IACR;IACA;IACA,QAAQ,WAAW;IACpB,CAAC;YACM;AACR,gBAAa,UAAU;;SAEnB;;AAKV,MAAa,eAAe,EAAE,MAAM"}
1
+ {"version":3,"file":"lambda-client.cjs","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"sourcesContent":["// Telemetry sink client.\n//\n// Posts events to a CopilotKit-controlled telemetry-sink endpoint, which\n// fans out to Scarf, Reo, and any future destinations. Replaces the direct\n// per-vendor calls (scarf-client.ts) so that vendor changes don't require\n// SDK releases and so that downstream services we don't want exposed to\n// OSS readers (e.g. the email-enrichment service backing Reo) stay\n// private.\n//\n// Two attribution modes:\n// - Identified: a standalone telemetry id or a CopilotKit license token is\n// configured. Standalone identity takes precedence. License tokens are\n// JWTs (header.payload.sig) whose payload carries `telemetry_id`; the SDK\n// base64url-decodes the payload — without verifying the Ed25519 signature,\n// which is the license-verifier's job. The resolved identity is emitted\n// only via `X-CopilotKit-Telemetry-Id`.\n// - Anonymous: no license token, or a malformed/non-JWT one. No\n// telemetry-id header; events still flow, attribution is best-effort\n// from request-level signals (IP, UA).\n//\n// Note: CopilotCloud customer API keys (`ck_<env>_<id>.<secret>`) are\n// unrelated to telemetry attribution. They flow into Segment / PostHog\n// via the v1 shared TelemetryClient and never reach this code path.\n//\n// Best-effort: every error is swallowed. Telemetry must not break the\n// host application.\n\nconst TELEMETRY_SINK_URL =\n (typeof process !== \"undefined\" && process.env?.COPILOTKIT_TELEMETRY_URL) ||\n \"https://telemetry.copilotkit.ai/ingest\";\n\nconst FETCH_TIMEOUT_MS = 3000;\nconst TELEMETRY_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;\n\nexport interface LambdaSendOptions {\n event: string;\n properties?: Record<string, unknown>;\n globalProperties?: Record<string, unknown>;\n packageName?: string;\n packageVersion?: string;\n /** Standalone analytics identity, resolved before any legacy license claim. */\n telemetryId?: string;\n // The CopilotKit license token (Ed25519-signed JWT), when one is\n // configured on the runtime. The sender base64url-decodes the payload\n // segment to extract `telemetry_id`; missing or malformed tokens\n // produce an anonymous send.\n licenseToken?: string;\n}\n\n/**\n * Return the first telemetry identity accepted by the ingest service.\n *\n * Empty and whitespace-only values are unconfigured placeholders and must not\n * suppress a later identity source. Leading and trailing HTTP spaces and tabs\n * are removed before validation. The ingest service accepts 1 to 128 ASCII\n * letters, digits, underscores, and hyphens.\n *\n * @internal\n */\nexport function firstNonBlankTelemetryId(\n ...candidates: ReadonlyArray<string | undefined>\n): string | undefined {\n for (const candidate of candidates) {\n if (candidate === undefined) {\n continue;\n }\n\n const normalized = candidate.replace(/^[\\t ]+|[\\t ]+$/g, \"\");\n if (TELEMETRY_ID_PATTERN.test(normalized)) {\n return normalized;\n }\n }\n\n return undefined;\n}\n\n// These fields aren't used by the telemetry service, so we strip them\n// at the wire boundary rather than rely on every caller to omit them.\n// Both the snake_case and camelCase variants are listed because callers\n// upstream use different conventions.\nconst STRIPPED_KEYS = new Set([\"cloud.public_api_key\", \"cloud.publicApiKey\"]);\n\nfunction stripCloudKeys(\n obj: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n if (!obj) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!STRIPPED_KEYS.has(k)) out[k] = v;\n }\n return out;\n}\n\n// Pull telemetry_id out of a CopilotKit license token without verifying\n// the signature. The token shape is a standard JWT\n// (`<header>.<payload>.<sig>`) with base64url-encoded segments; the\n// payload is UTF-8 JSON with a telemetry_id accepted by the ingest service.\n//\n// Verification (Ed25519, key rotation, expiry) is the license-verifier\n// package's job. For telemetry attribution we only need the claimed id —\n// the trust model is claim-only on the Lambda side anyway.\n//\n// Exported so TelemetryClient setters can detect unparseable tokens at\n// configuration time and surface a single warning, instead of silently\n// emitting anonymous events on every capture.\nexport function parseTelemetryIdFromLicense(token?: string): string | null {\n if (!token) return null;\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n try {\n const payload = parts[1];\n if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) {\n return null;\n }\n\n let b64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = (4 - (b64.length % 4)) % 4;\n b64 += \"=\".repeat(padding);\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(b64, \"base64\").toString(\"utf8\")\n : new TextDecoder().decode(\n Uint8Array.from(atob(b64), (character) => character.charCodeAt(0)),\n );\n const decoded = JSON.parse(json) as { telemetry_id?: unknown };\n const telemetryId =\n typeof decoded.telemetry_id === \"string\"\n ? decoded.telemetry_id\n : undefined;\n return firstNonBlankTelemetryId(telemetryId) ?? null;\n } catch {\n return null;\n }\n}\n\n// Parse the telemetry_id from a license token AND emit the rollout smoke\n// signal if the parse returned null. Returning the parsed id lets callers\n// cache it in one step (avoiding a second parseTelemetryIdFromLicense\n// pass) while keeping the warn text in lockstep between v1 (shared) and\n// v2 (runtime) TelemetryClient.setLicenseToken.\nexport function parseAndWarnTelemetryId(licenseToken: string): string | null {\n const telemetryId = parseTelemetryIdFromLicense(licenseToken);\n if (!telemetryId) {\n console.warn(\n \"[CopilotKit] License token did not yield a telemetry_id; telemetry events will be sent anonymously.\",\n );\n }\n return telemetryId;\n}\n\nexport async function send(opts: LambdaSendOptions): Promise<void> {\n try {\n const body = JSON.stringify({\n event: opts.event,\n properties: stripCloudKeys(opts.properties),\n global_properties: stripCloudKeys(opts.globalProperties),\n package: {\n name: opts.packageName,\n version: opts.packageVersion,\n },\n ts: Math.floor(Date.now() / 1000),\n });\n\n const telemetryId =\n firstNonBlankTelemetryId(opts.telemetryId) ??\n parseTelemetryIdFromLicense(opts.licenseToken);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": opts.packageName\n ? `CopilotKit-Runtime/${opts.packageVersion ?? \"unknown\"} (${opts.packageName})`\n : \"CopilotKit-Runtime\",\n };\n if (telemetryId) {\n headers[\"X-CopilotKit-Telemetry-Id\"] = telemetryId;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n await fetch(TELEMETRY_SINK_URL, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timeoutId);\n }\n } catch {\n // Silent failure — telemetry must not break the application.\n }\n}\n\nexport const lambdaClient = { send };\n"],"mappings":";;AA2BA,MAAM,qBACH,OAAO,YAAY,eAAe,QAAQ,KAAK,4BAChD;AAEF,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;;;;;;;;;;;AA2B7B,SAAgB,yBACd,GAAG,YACiB;AACpB,MAAK,MAAM,aAAa,YAAY;AAClC,MAAI,cAAc,OAChB;EAGF,MAAM,aAAa,UAAU,QAAQ,oBAAoB,GAAG;AAC5D,MAAI,qBAAqB,KAAK,WAAW,CACvC,QAAO;;;AAWb,MAAM,gBAAgB,IAAI,IAAI,CAAC,wBAAwB,qBAAqB,CAAC;AAE7E,SAAS,eACP,KACyB;AACzB,KAAI,CAAC,IAAK,QAAO,EAAE;CACnB,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CACtC,KAAI,CAAC,cAAc,IAAI,EAAE,CAAE,KAAI,KAAK;AAEtC,QAAO;;AAeT,SAAgB,4BAA4B,OAA+B;AACzE,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,KAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,KAAI;EACF,MAAM,UAAU,MAAM;AACtB,MAAI,CAAC,mBAAmB,KAAK,QAAQ,IAAI,QAAQ,SAAS,MAAM,EAC9D,QAAO;EAGT,IAAI,MAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI;EACvD,MAAM,WAAW,IAAK,IAAI,SAAS,KAAM;AACzC,SAAO,IAAI,OAAO,QAAQ;EAC1B,MAAM,OACJ,OAAO,WAAW,cACd,OAAO,KAAK,KAAK,SAAS,CAAC,SAAS,OAAO,GAC3C,IAAI,aAAa,CAAC,OAChB,WAAW,KAAK,KAAK,IAAI,GAAG,cAAc,UAAU,WAAW,EAAE,CAAC,CACnE;EACP,MAAM,UAAU,KAAK,MAAM,KAAK;AAKhC,SAAO,yBAHL,OAAO,QAAQ,iBAAiB,WAC5B,QAAQ,eACR,OACsC,IAAI;SAC1C;AACN,SAAO;;;AASX,SAAgB,wBAAwB,cAAqC;CAC3E,MAAM,cAAc,4BAA4B,aAAa;AAC7D,KAAI,CAAC,YACH,SAAQ,KACN,sGACD;AAEH,QAAO;;AAGT,eAAsB,KAAK,MAAwC;AACjE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU;GAC1B,OAAO,KAAK;GACZ,YAAY,eAAe,KAAK,WAAW;GAC3C,mBAAmB,eAAe,KAAK,iBAAiB;GACxD,SAAS;IACP,MAAM,KAAK;IACX,SAAS,KAAK;IACf;GACD,IAAI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAClC,CAAC;EAEF,MAAM,cACJ,yBAAyB,KAAK,YAAY,IAC1C,4BAA4B,KAAK,aAAa;EAChD,MAAM,UAAkC;GACtC,gBAAgB;GAChB,cAAc,KAAK,cACf,sBAAsB,KAAK,kBAAkB,UAAU,IAAI,KAAK,YAAY,KAC5E;GACL;AACD,MAAI,YACF,SAAQ,+BAA+B;EAGzC,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,iBAAiB;AACxE,MAAI;AACF,SAAM,MAAM,oBAAoB;IAC9B,QAAQ;IACR;IACA;IACA,QAAQ,WAAW;IACpB,CAAC;YACM;AACR,gBAAa,UAAU;;SAEnB;;AAKV,MAAa,eAAe,EAAE,MAAM"}
@@ -5,8 +5,21 @@ interface LambdaSendOptions {
5
5
  globalProperties?: Record<string, unknown>;
6
6
  packageName?: string;
7
7
  packageVersion?: string;
8
+ /** Standalone analytics identity, resolved before any legacy license claim. */
9
+ telemetryId?: string;
8
10
  licenseToken?: string;
9
11
  }
12
+ /**
13
+ * Return the first telemetry identity accepted by the ingest service.
14
+ *
15
+ * Empty and whitespace-only values are unconfigured placeholders and must not
16
+ * suppress a later identity source. Leading and trailing HTTP spaces and tabs
17
+ * are removed before validation. The ingest service accepts 1 to 128 ASCII
18
+ * letters, digits, underscores, and hyphens.
19
+ *
20
+ * @internal
21
+ */
22
+ declare function firstNonBlankTelemetryId(...candidates: ReadonlyArray<string | undefined>): string | undefined;
10
23
  declare function parseTelemetryIdFromLicense(token?: string): string | null;
11
24
  declare function parseAndWarnTelemetryId(licenseToken: string): string | null;
12
25
  declare function send(opts: LambdaSendOptions): Promise<void>;
@@ -14,5 +27,5 @@ declare const lambdaClient: {
14
27
  send: typeof send;
15
28
  };
16
29
  //#endregion
17
- export { LambdaSendOptions, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
30
+ export { LambdaSendOptions, firstNonBlankTelemetryId, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
18
31
  //# sourceMappingURL=lambda-client.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lambda-client.d.cts","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"mappings":";UAiCiB,iBAAA;EACf,KAAA;EACA,UAAA,GAAa,MAAA;EACb,gBAAA,GAAmB,MAAA;EACnB,WAAA;EACA,cAAA;EAKA,YAAA;AAAA;AAAA,iBAgCc,2BAAA,CAA4B,KAAA;AAAA,iBA0B5B,uBAAA,CAAwB,YAAA;AAAA,iBAUlB,IAAA,CAAK,IAAA,EAAM,iBAAA,GAAoB,OAAA;AAAA,cAyCxC,YAAA;eAAuB,IAAA;AAAA"}
1
+ {"version":3,"file":"lambda-client.d.cts","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"mappings":";UAkCiB,iBAAA;EACf,KAAA;EACA,UAAA,GAAa,MAAA;EACb,gBAAA,GAAmB,MAAA;EACnB,WAAA;EACA,cAAA;EAHA;EAKA,WAAA;EAKA,YAAA;AAAA;;;;;;;AAaF;;;;iBAAgB,wBAAA,CAAA,GACX,UAAA,EAAY,aAAA;AAAA,iBA6CD,2BAAA,CAA4B,KAAA;AAAA,iBAmC5B,uBAAA,CAAwB,YAAA;AAAA,iBAUlB,IAAA,CAAK,IAAA,EAAM,iBAAA,GAAoB,OAAA;AAAA,cA2CxC,YAAA;eAAuB,IAAA;AAAA"}
@@ -5,8 +5,21 @@ interface LambdaSendOptions {
5
5
  globalProperties?: Record<string, unknown>;
6
6
  packageName?: string;
7
7
  packageVersion?: string;
8
+ /** Standalone analytics identity, resolved before any legacy license claim. */
9
+ telemetryId?: string;
8
10
  licenseToken?: string;
9
11
  }
12
+ /**
13
+ * Return the first telemetry identity accepted by the ingest service.
14
+ *
15
+ * Empty and whitespace-only values are unconfigured placeholders and must not
16
+ * suppress a later identity source. Leading and trailing HTTP spaces and tabs
17
+ * are removed before validation. The ingest service accepts 1 to 128 ASCII
18
+ * letters, digits, underscores, and hyphens.
19
+ *
20
+ * @internal
21
+ */
22
+ declare function firstNonBlankTelemetryId(...candidates: ReadonlyArray<string | undefined>): string | undefined;
10
23
  declare function parseTelemetryIdFromLicense(token?: string): string | null;
11
24
  declare function parseAndWarnTelemetryId(licenseToken: string): string | null;
12
25
  declare function send(opts: LambdaSendOptions): Promise<void>;
@@ -14,5 +27,5 @@ declare const lambdaClient: {
14
27
  send: typeof send;
15
28
  };
16
29
  //#endregion
17
- export { LambdaSendOptions, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
30
+ export { LambdaSendOptions, firstNonBlankTelemetryId, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
18
31
  //# sourceMappingURL=lambda-client.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lambda-client.d.mts","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"mappings":";UAiCiB,iBAAA;EACf,KAAA;EACA,UAAA,GAAa,MAAA;EACb,gBAAA,GAAmB,MAAA;EACnB,WAAA;EACA,cAAA;EAKA,YAAA;AAAA;AAAA,iBAgCc,2BAAA,CAA4B,KAAA;AAAA,iBA0B5B,uBAAA,CAAwB,YAAA;AAAA,iBAUlB,IAAA,CAAK,IAAA,EAAM,iBAAA,GAAoB,OAAA;AAAA,cAyCxC,YAAA;eAAuB,IAAA;AAAA"}
1
+ {"version":3,"file":"lambda-client.d.mts","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"mappings":";UAkCiB,iBAAA;EACf,KAAA;EACA,UAAA,GAAa,MAAA;EACb,gBAAA,GAAmB,MAAA;EACnB,WAAA;EACA,cAAA;EAHA;EAKA,WAAA;EAKA,YAAA;AAAA;;;;;;;AAaF;;;;iBAAgB,wBAAA,CAAA,GACX,UAAA,EAAY,aAAA;AAAA,iBA6CD,2BAAA,CAA4B,KAAA;AAAA,iBAmC5B,uBAAA,CAAwB,YAAA;AAAA,iBAUlB,IAAA,CAAK,IAAA,EAAM,iBAAA,GAAoB,OAAA;AAAA,cA2CxC,YAAA;eAAuB,IAAA;AAAA"}
@@ -1,6 +1,24 @@
1
1
  //#region src/telemetry/lambda-client.ts
2
2
  const TELEMETRY_SINK_URL = typeof process !== "undefined" && process.env?.COPILOTKIT_TELEMETRY_URL || "https://telemetry.copilotkit.ai/ingest";
3
3
  const FETCH_TIMEOUT_MS = 3e3;
4
+ const TELEMETRY_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
5
+ /**
6
+ * Return the first telemetry identity accepted by the ingest service.
7
+ *
8
+ * Empty and whitespace-only values are unconfigured placeholders and must not
9
+ * suppress a later identity source. Leading and trailing HTTP spaces and tabs
10
+ * are removed before validation. The ingest service accepts 1 to 128 ASCII
11
+ * letters, digits, underscores, and hyphens.
12
+ *
13
+ * @internal
14
+ */
15
+ function firstNonBlankTelemetryId(...candidates) {
16
+ for (const candidate of candidates) {
17
+ if (candidate === void 0) continue;
18
+ const normalized = candidate.replace(/^[\t ]+|[\t ]+$/g, "");
19
+ if (TELEMETRY_ID_PATTERN.test(normalized)) return normalized;
20
+ }
21
+ }
4
22
  const STRIPPED_KEYS = new Set(["cloud.public_api_key", "cloud.publicApiKey"]);
5
23
  function stripCloudKeys(obj) {
6
24
  if (!obj) return {};
@@ -13,12 +31,14 @@ function parseTelemetryIdFromLicense(token) {
13
31
  const parts = token.split(".");
14
32
  if (parts.length !== 3) return null;
15
33
  try {
16
- let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
34
+ const payload = parts[1];
35
+ if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) return null;
36
+ let b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
17
37
  const padding = (4 - b64.length % 4) % 4;
18
38
  b64 += "=".repeat(padding);
19
- const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("utf8");
39
+ const json = typeof Buffer !== "undefined" ? Buffer.from(b64, "base64").toString("utf8") : new TextDecoder().decode(Uint8Array.from(atob(b64), (character) => character.charCodeAt(0)));
20
40
  const decoded = JSON.parse(json);
21
- return typeof decoded.telemetry_id === "string" ? decoded.telemetry_id : null;
41
+ return firstNonBlankTelemetryId(typeof decoded.telemetry_id === "string" ? decoded.telemetry_id : void 0) ?? null;
22
42
  } catch {
23
43
  return null;
24
44
  }
@@ -40,7 +60,7 @@ async function send(opts) {
40
60
  },
41
61
  ts: Math.floor(Date.now() / 1e3)
42
62
  });
43
- const telemetryId = parseTelemetryIdFromLicense(opts.licenseToken);
63
+ const telemetryId = firstNonBlankTelemetryId(opts.telemetryId) ?? parseTelemetryIdFromLicense(opts.licenseToken);
44
64
  const headers = {
45
65
  "Content-Type": "application/json",
46
66
  "User-Agent": opts.packageName ? `CopilotKit-Runtime/${opts.packageVersion ?? "unknown"} (${opts.packageName})` : "CopilotKit-Runtime"
@@ -63,5 +83,5 @@ async function send(opts) {
63
83
  const lambdaClient = { send };
64
84
 
65
85
  //#endregion
66
- export { lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
86
+ export { firstNonBlankTelemetryId, lambdaClient, parseAndWarnTelemetryId, parseTelemetryIdFromLicense };
67
87
  //# sourceMappingURL=lambda-client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"lambda-client.mjs","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"sourcesContent":["// Telemetry sink client.\n//\n// Posts events to a CopilotKit-controlled telemetry-sink endpoint, which\n// fans out to Scarf, Reo, and any future destinations. Replaces the direct\n// per-vendor calls (scarf-client.ts) so that vendor changes don't require\n// SDK releases and so that downstream services we don't want exposed to\n// OSS readers (e.g. the email-enrichment service backing Reo) stay\n// private.\n//\n// Two attribution modes:\n// - Identified: a CopilotKit license token is configured. The token is\n// a JWT (header.payload.sig) whose payload carries `telemetry_id`.\n// The SDK base64url-decodes the payload — without verifying the\n// Ed25519 signature, which is the license-verifier's job and\n// emits the id via `X-CopilotKit-Telemetry-Id`. The Lambda uses it\n// to enrich events with the customer's email.\n// - Anonymous: no license token, or a malformed/non-JWT one. No\n// telemetry-id header; events still flow, attribution is best-effort\n// from request-level signals (IP, UA).\n//\n// Note: CopilotCloud customer API keys (`ck_<env>_<id>.<secret>`) are\n// unrelated to telemetry attribution. They flow into Segment / PostHog\n// via the v1 shared TelemetryClient and never reach this code path.\n//\n// Best-effort: every error is swallowed. Telemetry must not break the\n// host application.\n\nconst TELEMETRY_SINK_URL =\n (typeof process !== \"undefined\" && process.env?.COPILOTKIT_TELEMETRY_URL) ||\n \"https://telemetry.copilotkit.ai/ingest\";\n\nconst FETCH_TIMEOUT_MS = 3000;\n\nexport interface LambdaSendOptions {\n event: string;\n properties?: Record<string, unknown>;\n globalProperties?: Record<string, unknown>;\n packageName?: string;\n packageVersion?: string;\n // The CopilotKit license token (Ed25519-signed JWT), when one is\n // configured on the runtime. The sender base64url-decodes the payload\n // segment to extract `telemetry_id`; missing or malformed tokens\n // produce an anonymous send.\n licenseToken?: string;\n}\n\n// These fields aren't used by the telemetry service, so we strip them\n// at the wire boundary rather than rely on every caller to omit them.\n// Both the snake_case and camelCase variants are listed because callers\n// upstream use different conventions.\nconst STRIPPED_KEYS = new Set([\"cloud.public_api_key\", \"cloud.publicApiKey\"]);\n\nfunction stripCloudKeys(\n obj: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n if (!obj) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!STRIPPED_KEYS.has(k)) out[k] = v;\n }\n return out;\n}\n\n// Pull telemetry_id out of a CopilotKit license token without verifying\n// the signature. The token shape is a standard JWT\n// (`<header>.<payload>.<sig>`) with base64url-encoded segments; the\n// payload is JSON with a `telemetry_id` string field.\n//\n// Verification (Ed25519, key rotation, expiry) is the license-verifier\n// package's job. For telemetry attribution we only need the claimed id —\n// the trust model is claim-only on the Lambda side anyway.\n//\n// Exported so TelemetryClient setters can detect unparseable tokens at\n// configuration time and surface a single warning, instead of silently\n// emitting anonymous events on every capture.\nexport function parseTelemetryIdFromLicense(token?: string): string | null {\n if (!token) return null;\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n try {\n let b64 = parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = (4 - (b64.length % 4)) % 4;\n b64 += \"=\".repeat(padding);\n const json =\n typeof atob === \"function\"\n ? atob(b64)\n : Buffer.from(b64, \"base64\").toString(\"utf8\");\n const decoded = JSON.parse(json) as { telemetry_id?: unknown };\n return typeof decoded.telemetry_id === \"string\"\n ? decoded.telemetry_id\n : null;\n } catch {\n return null;\n }\n}\n\n// Parse the telemetry_id from a license token AND emit the rollout smoke\n// signal if the parse returned null. Returning the parsed id lets callers\n// cache it in one step (avoiding a second parseTelemetryIdFromLicense\n// pass) while keeping the warn text in lockstep between v1 (shared) and\n// v2 (runtime) TelemetryClient.setLicenseToken.\nexport function parseAndWarnTelemetryId(licenseToken: string): string | null {\n const telemetryId = parseTelemetryIdFromLicense(licenseToken);\n if (!telemetryId) {\n console.warn(\n \"[CopilotKit] License token did not yield a telemetry_id; telemetry events will be sent anonymously.\",\n );\n }\n return telemetryId;\n}\n\nexport async function send(opts: LambdaSendOptions): Promise<void> {\n try {\n const body = JSON.stringify({\n event: opts.event,\n properties: stripCloudKeys(opts.properties),\n global_properties: stripCloudKeys(opts.globalProperties),\n package: {\n name: opts.packageName,\n version: opts.packageVersion,\n },\n ts: Math.floor(Date.now() / 1000),\n });\n\n const telemetryId = parseTelemetryIdFromLicense(opts.licenseToken);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": opts.packageName\n ? `CopilotKit-Runtime/${opts.packageVersion ?? \"unknown\"} (${opts.packageName})`\n : \"CopilotKit-Runtime\",\n };\n if (telemetryId) {\n headers[\"X-CopilotKit-Telemetry-Id\"] = telemetryId;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n await fetch(TELEMETRY_SINK_URL, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timeoutId);\n }\n } catch {\n // Silent failure — telemetry must not break the application.\n }\n}\n\nexport const lambdaClient = { send };\n"],"mappings":";AA2BA,MAAM,qBACH,OAAO,YAAY,eAAe,QAAQ,KAAK,4BAChD;AAEF,MAAM,mBAAmB;AAmBzB,MAAM,gBAAgB,IAAI,IAAI,CAAC,wBAAwB,qBAAqB,CAAC;AAE7E,SAAS,eACP,KACyB;AACzB,KAAI,CAAC,IAAK,QAAO,EAAE;CACnB,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CACtC,KAAI,CAAC,cAAc,IAAI,EAAE,CAAE,KAAI,KAAK;AAEtC,QAAO;;AAeT,SAAgB,4BAA4B,OAA+B;AACzE,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,KAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,KAAI;EACF,IAAI,MAAM,MAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI;EACxD,MAAM,WAAW,IAAK,IAAI,SAAS,KAAM;AACzC,SAAO,IAAI,OAAO,QAAQ;EAC1B,MAAM,OACJ,OAAO,SAAS,aACZ,KAAK,IAAI,GACT,OAAO,KAAK,KAAK,SAAS,CAAC,SAAS,OAAO;EACjD,MAAM,UAAU,KAAK,MAAM,KAAK;AAChC,SAAO,OAAO,QAAQ,iBAAiB,WACnC,QAAQ,eACR;SACE;AACN,SAAO;;;AASX,SAAgB,wBAAwB,cAAqC;CAC3E,MAAM,cAAc,4BAA4B,aAAa;AAC7D,KAAI,CAAC,YACH,SAAQ,KACN,sGACD;AAEH,QAAO;;AAGT,eAAsB,KAAK,MAAwC;AACjE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU;GAC1B,OAAO,KAAK;GACZ,YAAY,eAAe,KAAK,WAAW;GAC3C,mBAAmB,eAAe,KAAK,iBAAiB;GACxD,SAAS;IACP,MAAM,KAAK;IACX,SAAS,KAAK;IACf;GACD,IAAI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAClC,CAAC;EAEF,MAAM,cAAc,4BAA4B,KAAK,aAAa;EAClE,MAAM,UAAkC;GACtC,gBAAgB;GAChB,cAAc,KAAK,cACf,sBAAsB,KAAK,kBAAkB,UAAU,IAAI,KAAK,YAAY,KAC5E;GACL;AACD,MAAI,YACF,SAAQ,+BAA+B;EAGzC,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,iBAAiB;AACxE,MAAI;AACF,SAAM,MAAM,oBAAoB;IAC9B,QAAQ;IACR;IACA;IACA,QAAQ,WAAW;IACpB,CAAC;YACM;AACR,gBAAa,UAAU;;SAEnB;;AAKV,MAAa,eAAe,EAAE,MAAM"}
1
+ {"version":3,"file":"lambda-client.mjs","names":[],"sources":["../../src/telemetry/lambda-client.ts"],"sourcesContent":["// Telemetry sink client.\n//\n// Posts events to a CopilotKit-controlled telemetry-sink endpoint, which\n// fans out to Scarf, Reo, and any future destinations. Replaces the direct\n// per-vendor calls (scarf-client.ts) so that vendor changes don't require\n// SDK releases and so that downstream services we don't want exposed to\n// OSS readers (e.g. the email-enrichment service backing Reo) stay\n// private.\n//\n// Two attribution modes:\n// - Identified: a standalone telemetry id or a CopilotKit license token is\n// configured. Standalone identity takes precedence. License tokens are\n// JWTs (header.payload.sig) whose payload carries `telemetry_id`; the SDK\n// base64url-decodes the payload — without verifying the Ed25519 signature,\n// which is the license-verifier's job. The resolved identity is emitted\n// only via `X-CopilotKit-Telemetry-Id`.\n// - Anonymous: no license token, or a malformed/non-JWT one. No\n// telemetry-id header; events still flow, attribution is best-effort\n// from request-level signals (IP, UA).\n//\n// Note: CopilotCloud customer API keys (`ck_<env>_<id>.<secret>`) are\n// unrelated to telemetry attribution. They flow into Segment / PostHog\n// via the v1 shared TelemetryClient and never reach this code path.\n//\n// Best-effort: every error is swallowed. Telemetry must not break the\n// host application.\n\nconst TELEMETRY_SINK_URL =\n (typeof process !== \"undefined\" && process.env?.COPILOTKIT_TELEMETRY_URL) ||\n \"https://telemetry.copilotkit.ai/ingest\";\n\nconst FETCH_TIMEOUT_MS = 3000;\nconst TELEMETRY_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;\n\nexport interface LambdaSendOptions {\n event: string;\n properties?: Record<string, unknown>;\n globalProperties?: Record<string, unknown>;\n packageName?: string;\n packageVersion?: string;\n /** Standalone analytics identity, resolved before any legacy license claim. */\n telemetryId?: string;\n // The CopilotKit license token (Ed25519-signed JWT), when one is\n // configured on the runtime. The sender base64url-decodes the payload\n // segment to extract `telemetry_id`; missing or malformed tokens\n // produce an anonymous send.\n licenseToken?: string;\n}\n\n/**\n * Return the first telemetry identity accepted by the ingest service.\n *\n * Empty and whitespace-only values are unconfigured placeholders and must not\n * suppress a later identity source. Leading and trailing HTTP spaces and tabs\n * are removed before validation. The ingest service accepts 1 to 128 ASCII\n * letters, digits, underscores, and hyphens.\n *\n * @internal\n */\nexport function firstNonBlankTelemetryId(\n ...candidates: ReadonlyArray<string | undefined>\n): string | undefined {\n for (const candidate of candidates) {\n if (candidate === undefined) {\n continue;\n }\n\n const normalized = candidate.replace(/^[\\t ]+|[\\t ]+$/g, \"\");\n if (TELEMETRY_ID_PATTERN.test(normalized)) {\n return normalized;\n }\n }\n\n return undefined;\n}\n\n// These fields aren't used by the telemetry service, so we strip them\n// at the wire boundary rather than rely on every caller to omit them.\n// Both the snake_case and camelCase variants are listed because callers\n// upstream use different conventions.\nconst STRIPPED_KEYS = new Set([\"cloud.public_api_key\", \"cloud.publicApiKey\"]);\n\nfunction stripCloudKeys(\n obj: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n if (!obj) return {};\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!STRIPPED_KEYS.has(k)) out[k] = v;\n }\n return out;\n}\n\n// Pull telemetry_id out of a CopilotKit license token without verifying\n// the signature. The token shape is a standard JWT\n// (`<header>.<payload>.<sig>`) with base64url-encoded segments; the\n// payload is UTF-8 JSON with a telemetry_id accepted by the ingest service.\n//\n// Verification (Ed25519, key rotation, expiry) is the license-verifier\n// package's job. For telemetry attribution we only need the claimed id —\n// the trust model is claim-only on the Lambda side anyway.\n//\n// Exported so TelemetryClient setters can detect unparseable tokens at\n// configuration time and surface a single warning, instead of silently\n// emitting anonymous events on every capture.\nexport function parseTelemetryIdFromLicense(token?: string): string | null {\n if (!token) return null;\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n try {\n const payload = parts[1];\n if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) {\n return null;\n }\n\n let b64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padding = (4 - (b64.length % 4)) % 4;\n b64 += \"=\".repeat(padding);\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(b64, \"base64\").toString(\"utf8\")\n : new TextDecoder().decode(\n Uint8Array.from(atob(b64), (character) => character.charCodeAt(0)),\n );\n const decoded = JSON.parse(json) as { telemetry_id?: unknown };\n const telemetryId =\n typeof decoded.telemetry_id === \"string\"\n ? decoded.telemetry_id\n : undefined;\n return firstNonBlankTelemetryId(telemetryId) ?? null;\n } catch {\n return null;\n }\n}\n\n// Parse the telemetry_id from a license token AND emit the rollout smoke\n// signal if the parse returned null. Returning the parsed id lets callers\n// cache it in one step (avoiding a second parseTelemetryIdFromLicense\n// pass) while keeping the warn text in lockstep between v1 (shared) and\n// v2 (runtime) TelemetryClient.setLicenseToken.\nexport function parseAndWarnTelemetryId(licenseToken: string): string | null {\n const telemetryId = parseTelemetryIdFromLicense(licenseToken);\n if (!telemetryId) {\n console.warn(\n \"[CopilotKit] License token did not yield a telemetry_id; telemetry events will be sent anonymously.\",\n );\n }\n return telemetryId;\n}\n\nexport async function send(opts: LambdaSendOptions): Promise<void> {\n try {\n const body = JSON.stringify({\n event: opts.event,\n properties: stripCloudKeys(opts.properties),\n global_properties: stripCloudKeys(opts.globalProperties),\n package: {\n name: opts.packageName,\n version: opts.packageVersion,\n },\n ts: Math.floor(Date.now() / 1000),\n });\n\n const telemetryId =\n firstNonBlankTelemetryId(opts.telemetryId) ??\n parseTelemetryIdFromLicense(opts.licenseToken);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": opts.packageName\n ? `CopilotKit-Runtime/${opts.packageVersion ?? \"unknown\"} (${opts.packageName})`\n : \"CopilotKit-Runtime\",\n };\n if (telemetryId) {\n headers[\"X-CopilotKit-Telemetry-Id\"] = telemetryId;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n await fetch(TELEMETRY_SINK_URL, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timeoutId);\n }\n } catch {\n // Silent failure — telemetry must not break the application.\n }\n}\n\nexport const lambdaClient = { send };\n"],"mappings":";AA2BA,MAAM,qBACH,OAAO,YAAY,eAAe,QAAQ,KAAK,4BAChD;AAEF,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;;;;;;;;;;;AA2B7B,SAAgB,yBACd,GAAG,YACiB;AACpB,MAAK,MAAM,aAAa,YAAY;AAClC,MAAI,cAAc,OAChB;EAGF,MAAM,aAAa,UAAU,QAAQ,oBAAoB,GAAG;AAC5D,MAAI,qBAAqB,KAAK,WAAW,CACvC,QAAO;;;AAWb,MAAM,gBAAgB,IAAI,IAAI,CAAC,wBAAwB,qBAAqB,CAAC;AAE7E,SAAS,eACP,KACyB;AACzB,KAAI,CAAC,IAAK,QAAO,EAAE;CACnB,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CACtC,KAAI,CAAC,cAAc,IAAI,EAAE,CAAE,KAAI,KAAK;AAEtC,QAAO;;AAeT,SAAgB,4BAA4B,OAA+B;AACzE,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,KAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,KAAI;EACF,MAAM,UAAU,MAAM;AACtB,MAAI,CAAC,mBAAmB,KAAK,QAAQ,IAAI,QAAQ,SAAS,MAAM,EAC9D,QAAO;EAGT,IAAI,MAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI;EACvD,MAAM,WAAW,IAAK,IAAI,SAAS,KAAM;AACzC,SAAO,IAAI,OAAO,QAAQ;EAC1B,MAAM,OACJ,OAAO,WAAW,cACd,OAAO,KAAK,KAAK,SAAS,CAAC,SAAS,OAAO,GAC3C,IAAI,aAAa,CAAC,OAChB,WAAW,KAAK,KAAK,IAAI,GAAG,cAAc,UAAU,WAAW,EAAE,CAAC,CACnE;EACP,MAAM,UAAU,KAAK,MAAM,KAAK;AAKhC,SAAO,yBAHL,OAAO,QAAQ,iBAAiB,WAC5B,QAAQ,eACR,OACsC,IAAI;SAC1C;AACN,SAAO;;;AASX,SAAgB,wBAAwB,cAAqC;CAC3E,MAAM,cAAc,4BAA4B,aAAa;AAC7D,KAAI,CAAC,YACH,SAAQ,KACN,sGACD;AAEH,QAAO;;AAGT,eAAsB,KAAK,MAAwC;AACjE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU;GAC1B,OAAO,KAAK;GACZ,YAAY,eAAe,KAAK,WAAW;GAC3C,mBAAmB,eAAe,KAAK,iBAAiB;GACxD,SAAS;IACP,MAAM,KAAK;IACX,SAAS,KAAK;IACf;GACD,IAAI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAClC,CAAC;EAEF,MAAM,cACJ,yBAAyB,KAAK,YAAY,IAC1C,4BAA4B,KAAK,aAAa;EAChD,MAAM,UAAkC;GACtC,gBAAgB;GAChB,cAAc,KAAK,cACf,sBAAsB,KAAK,kBAAkB,UAAU,IAAI,KAAK,YAAY,KAC5E;GACL;AACD,MAAI,YACF,SAAQ,+BAA+B;EAGzC,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,iBAAiB;AACxE,MAAI;AACF,SAAM,MAAM,oBAAoB;IAC9B,QAAQ;IACR;IACA;IACA,QAAQ,WAAW;IACpB,CAAC;YACM;AACR,gBAAa,UAAU;;SAEnB;;AAKV,MAAa,eAAe,EAAE,MAAM"}
@@ -0,0 +1,28 @@
1
+
2
+ //#region src/telemetry/sampling.ts
3
+ /** Identifies which client emitted an event. */
4
+ const TELEMETRY_EMITTER_V1 = "v1-shared";
5
+ const TELEMETRY_EMITTER_V2 = "v2-runtime";
6
+ /**
7
+ * Compute the sampling block for one captured event.
8
+ *
9
+ * `telemetryId` is the caller's parsed license telemetry_id, or null when
10
+ * anonymous — it decides the branch, and is deliberately not returned:
11
+ * only the non-PII shape below travels on the event.
12
+ */
13
+ function computeSamplingMeta({ telemetryId, sampleRate }) {
14
+ const identified = Boolean(telemetryId);
15
+ const effectiveSampleRate = identified ? 1 : sampleRate;
16
+ return {
17
+ sampleRate: effectiveSampleRate,
18
+ sampleRateAdjustmentFactor: 1 - effectiveSampleRate,
19
+ sampleWeight: 1 / effectiveSampleRate,
20
+ telemetry_identified: identified
21
+ };
22
+ }
23
+
24
+ //#endregion
25
+ exports.TELEMETRY_EMITTER_V1 = TELEMETRY_EMITTER_V1;
26
+ exports.TELEMETRY_EMITTER_V2 = TELEMETRY_EMITTER_V2;
27
+ exports.computeSamplingMeta = computeSamplingMeta;
28
+ //# sourceMappingURL=sampling.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampling.cjs","names":[],"sources":["../../src/telemetry/sampling.ts"],"sourcesContent":["// Per-event telemetry metadata shared by both TelemetryClients.\n//\n// Two clients emit `oss.runtime.*`: the v1 client in this package and the\n// v2 client in @copilotkit/runtime. Both gate anonymous events at\n// `sampleRate` and let identified callers (a license token that yielded a\n// telemetry_id) through at 100%, so real volume can only be recovered\n// downstream as sum(sampleWeight) — a flat multiplier is wrong whenever\n// the identified share moves, which it does as Intelligence keys roll out.\n//\n// The two clients computed this independently and drifted: v2 sampled but\n// stamped nothing, leaving ~24% of runtime volume unweightable from the\n// data alone (OSS-1017). Computing it here is what stops them drifting\n// again.\n\n/** Identifies which client emitted an event. */\nexport const TELEMETRY_EMITTER_V1 = \"v1-shared\";\nexport const TELEMETRY_EMITTER_V2 = \"v2-runtime\";\n\nexport type TelemetryEmitter =\n | typeof TELEMETRY_EMITTER_V1\n | typeof TELEMETRY_EMITTER_V2;\n\n/**\n * Which wire an event copy travelled on. The v1 client sends each capture\n * to both, so this is what tells the two copies apart downstream\n * (OSS-1019); the v2 client only ever sends to the lambda sink.\n */\nexport type TelemetryTransport = \"segment\" | \"lambda\";\n\nexport interface SamplingMeta {\n /** The rate this event was actually gated at: 1 when identified. */\n sampleRate: number;\n sampleRateAdjustmentFactor: number;\n /** Multiply by this to extrapolate the population the event stands for. */\n sampleWeight: number;\n /** Whether the event bypassed the sample gate. */\n telemetry_identified: boolean;\n}\n\n/**\n * Compute the sampling block for one captured event.\n *\n * `telemetryId` is the caller's parsed license telemetry_id, or null when\n * anonymous — it decides the branch, and is deliberately not returned:\n * only the non-PII shape below travels on the event.\n */\nexport function computeSamplingMeta({\n telemetryId,\n sampleRate,\n}: {\n telemetryId: string | null;\n sampleRate: number;\n}): SamplingMeta {\n const identified = Boolean(telemetryId);\n // Identified events ship at a 100% effective rate, anonymous ones at\n // sampleRate. Computed per event because a single global weight would\n // overweight identified-customer counts by 1/sampleRate.\n const effectiveSampleRate = identified ? 1 : sampleRate;\n\n return {\n sampleRate: effectiveSampleRate,\n sampleRateAdjustmentFactor: 1 - effectiveSampleRate,\n sampleWeight: 1 / effectiveSampleRate,\n // Stated outright rather than inferred from sampleWeight === 1:\n // under COPILOTKIT_TELEMETRY_SAMPLE_RATE=1 anonymous events also\n // weigh 1, and the two populations stop being distinguishable\n // (OSS-1018).\n telemetry_identified: identified,\n };\n}\n"],"mappings":";;;AAeA,MAAa,uBAAuB;AACpC,MAAa,uBAAuB;;;;;;;;AA8BpC,SAAgB,oBAAoB,EAClC,aACA,cAIe;CACf,MAAM,aAAa,QAAQ,YAAY;CAIvC,MAAM,sBAAsB,aAAa,IAAI;AAE7C,QAAO;EACL,YAAY;EACZ,4BAA4B,IAAI;EAChC,cAAc,IAAI;EAKlB,sBAAsB;EACvB"}
@@ -0,0 +1,37 @@
1
+ //#region src/telemetry/sampling.d.ts
2
+ /** Identifies which client emitted an event. */
3
+ declare const TELEMETRY_EMITTER_V1 = "v1-shared";
4
+ declare const TELEMETRY_EMITTER_V2 = "v2-runtime";
5
+ type TelemetryEmitter = typeof TELEMETRY_EMITTER_V1 | typeof TELEMETRY_EMITTER_V2;
6
+ /**
7
+ * Which wire an event copy travelled on. The v1 client sends each capture
8
+ * to both, so this is what tells the two copies apart downstream
9
+ * (OSS-1019); the v2 client only ever sends to the lambda sink.
10
+ */
11
+ type TelemetryTransport = "segment" | "lambda";
12
+ interface SamplingMeta {
13
+ /** The rate this event was actually gated at: 1 when identified. */
14
+ sampleRate: number;
15
+ sampleRateAdjustmentFactor: number;
16
+ /** Multiply by this to extrapolate the population the event stands for. */
17
+ sampleWeight: number;
18
+ /** Whether the event bypassed the sample gate. */
19
+ telemetry_identified: boolean;
20
+ }
21
+ /**
22
+ * Compute the sampling block for one captured event.
23
+ *
24
+ * `telemetryId` is the caller's parsed license telemetry_id, or null when
25
+ * anonymous — it decides the branch, and is deliberately not returned:
26
+ * only the non-PII shape below travels on the event.
27
+ */
28
+ declare function computeSamplingMeta({
29
+ telemetryId,
30
+ sampleRate
31
+ }: {
32
+ telemetryId: string | null;
33
+ sampleRate: number;
34
+ }): SamplingMeta;
35
+ //#endregion
36
+ export { SamplingMeta, TELEMETRY_EMITTER_V1, TELEMETRY_EMITTER_V2, TelemetryEmitter, TelemetryTransport, computeSamplingMeta };
37
+ //# sourceMappingURL=sampling.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampling.d.cts","names":[],"sources":["../../src/telemetry/sampling.ts"],"mappings":";;cAea,oBAAA;AAAA,cACA,oBAAA;AAAA,KAED,gBAAA,UACD,oBAAA,UACA,oBAAA;;;AAJX;;;KAWY,kBAAA;AAAA,UAEK,YAAA;EAXL;EAaV,UAAA;EACA,0BAAA;EAbS;EAeT,YAAA;EAPU;EASV,oBAAA;AAAA;;;AAPF;;;;;iBAiBgB,mBAAA,CAAA;EACd,WAAA;EACA;AAAA;EAEA,WAAA;EACA,UAAA;AAAA,IACE,YAAA"}
@@ -0,0 +1,37 @@
1
+ //#region src/telemetry/sampling.d.ts
2
+ /** Identifies which client emitted an event. */
3
+ declare const TELEMETRY_EMITTER_V1 = "v1-shared";
4
+ declare const TELEMETRY_EMITTER_V2 = "v2-runtime";
5
+ type TelemetryEmitter = typeof TELEMETRY_EMITTER_V1 | typeof TELEMETRY_EMITTER_V2;
6
+ /**
7
+ * Which wire an event copy travelled on. The v1 client sends each capture
8
+ * to both, so this is what tells the two copies apart downstream
9
+ * (OSS-1019); the v2 client only ever sends to the lambda sink.
10
+ */
11
+ type TelemetryTransport = "segment" | "lambda";
12
+ interface SamplingMeta {
13
+ /** The rate this event was actually gated at: 1 when identified. */
14
+ sampleRate: number;
15
+ sampleRateAdjustmentFactor: number;
16
+ /** Multiply by this to extrapolate the population the event stands for. */
17
+ sampleWeight: number;
18
+ /** Whether the event bypassed the sample gate. */
19
+ telemetry_identified: boolean;
20
+ }
21
+ /**
22
+ * Compute the sampling block for one captured event.
23
+ *
24
+ * `telemetryId` is the caller's parsed license telemetry_id, or null when
25
+ * anonymous — it decides the branch, and is deliberately not returned:
26
+ * only the non-PII shape below travels on the event.
27
+ */
28
+ declare function computeSamplingMeta({
29
+ telemetryId,
30
+ sampleRate
31
+ }: {
32
+ telemetryId: string | null;
33
+ sampleRate: number;
34
+ }): SamplingMeta;
35
+ //#endregion
36
+ export { SamplingMeta, TELEMETRY_EMITTER_V1, TELEMETRY_EMITTER_V2, TelemetryEmitter, TelemetryTransport, computeSamplingMeta };
37
+ //# sourceMappingURL=sampling.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampling.d.mts","names":[],"sources":["../../src/telemetry/sampling.ts"],"mappings":";;cAea,oBAAA;AAAA,cACA,oBAAA;AAAA,KAED,gBAAA,UACD,oBAAA,UACA,oBAAA;;;AAJX;;;KAWY,kBAAA;AAAA,UAEK,YAAA;EAXL;EAaV,UAAA;EACA,0BAAA;EAbS;EAeT,YAAA;EAPU;EASV,oBAAA;AAAA;;;AAPF;;;;;iBAiBgB,mBAAA,CAAA;EACd,WAAA;EACA;AAAA;EAEA,WAAA;EACA,UAAA;AAAA,IACE,YAAA"}
@@ -0,0 +1,25 @@
1
+ //#region src/telemetry/sampling.ts
2
+ /** Identifies which client emitted an event. */
3
+ const TELEMETRY_EMITTER_V1 = "v1-shared";
4
+ const TELEMETRY_EMITTER_V2 = "v2-runtime";
5
+ /**
6
+ * Compute the sampling block for one captured event.
7
+ *
8
+ * `telemetryId` is the caller's parsed license telemetry_id, or null when
9
+ * anonymous — it decides the branch, and is deliberately not returned:
10
+ * only the non-PII shape below travels on the event.
11
+ */
12
+ function computeSamplingMeta({ telemetryId, sampleRate }) {
13
+ const identified = Boolean(telemetryId);
14
+ const effectiveSampleRate = identified ? 1 : sampleRate;
15
+ return {
16
+ sampleRate: effectiveSampleRate,
17
+ sampleRateAdjustmentFactor: 1 - effectiveSampleRate,
18
+ sampleWeight: 1 / effectiveSampleRate,
19
+ telemetry_identified: identified
20
+ };
21
+ }
22
+
23
+ //#endregion
24
+ export { TELEMETRY_EMITTER_V1, TELEMETRY_EMITTER_V2, computeSamplingMeta };
25
+ //# sourceMappingURL=sampling.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampling.mjs","names":[],"sources":["../../src/telemetry/sampling.ts"],"sourcesContent":["// Per-event telemetry metadata shared by both TelemetryClients.\n//\n// Two clients emit `oss.runtime.*`: the v1 client in this package and the\n// v2 client in @copilotkit/runtime. Both gate anonymous events at\n// `sampleRate` and let identified callers (a license token that yielded a\n// telemetry_id) through at 100%, so real volume can only be recovered\n// downstream as sum(sampleWeight) — a flat multiplier is wrong whenever\n// the identified share moves, which it does as Intelligence keys roll out.\n//\n// The two clients computed this independently and drifted: v2 sampled but\n// stamped nothing, leaving ~24% of runtime volume unweightable from the\n// data alone (OSS-1017). Computing it here is what stops them drifting\n// again.\n\n/** Identifies which client emitted an event. */\nexport const TELEMETRY_EMITTER_V1 = \"v1-shared\";\nexport const TELEMETRY_EMITTER_V2 = \"v2-runtime\";\n\nexport type TelemetryEmitter =\n | typeof TELEMETRY_EMITTER_V1\n | typeof TELEMETRY_EMITTER_V2;\n\n/**\n * Which wire an event copy travelled on. The v1 client sends each capture\n * to both, so this is what tells the two copies apart downstream\n * (OSS-1019); the v2 client only ever sends to the lambda sink.\n */\nexport type TelemetryTransport = \"segment\" | \"lambda\";\n\nexport interface SamplingMeta {\n /** The rate this event was actually gated at: 1 when identified. */\n sampleRate: number;\n sampleRateAdjustmentFactor: number;\n /** Multiply by this to extrapolate the population the event stands for. */\n sampleWeight: number;\n /** Whether the event bypassed the sample gate. */\n telemetry_identified: boolean;\n}\n\n/**\n * Compute the sampling block for one captured event.\n *\n * `telemetryId` is the caller's parsed license telemetry_id, or null when\n * anonymous — it decides the branch, and is deliberately not returned:\n * only the non-PII shape below travels on the event.\n */\nexport function computeSamplingMeta({\n telemetryId,\n sampleRate,\n}: {\n telemetryId: string | null;\n sampleRate: number;\n}): SamplingMeta {\n const identified = Boolean(telemetryId);\n // Identified events ship at a 100% effective rate, anonymous ones at\n // sampleRate. Computed per event because a single global weight would\n // overweight identified-customer counts by 1/sampleRate.\n const effectiveSampleRate = identified ? 1 : sampleRate;\n\n return {\n sampleRate: effectiveSampleRate,\n sampleRateAdjustmentFactor: 1 - effectiveSampleRate,\n sampleWeight: 1 / effectiveSampleRate,\n // Stated outright rather than inferred from sampleWeight === 1:\n // under COPILOTKIT_TELEMETRY_SAMPLE_RATE=1 anonymous events also\n // weigh 1, and the two populations stop being distinguishable\n // (OSS-1018).\n telemetry_identified: identified,\n };\n}\n"],"mappings":";;AAeA,MAAa,uBAAuB;AACpC,MAAa,uBAAuB;;;;;;;;AA8BpC,SAAgB,oBAAoB,EAClC,aACA,cAIe;CACf,MAAM,aAAa,QAAQ,YAAY;CAIvC,MAAM,sBAAsB,aAAa,IAAI;AAE7C,QAAO;EACL,YAAY;EACZ,4BAA4B,IAAI;EAChC,cAAc,IAAI;EAKlB,sBAAsB;EACvB"}
@@ -1,6 +1,7 @@
1
1
  const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
2
2
  const require_utils = require('./utils.cjs');
3
3
  const require_lambda_client = require('./lambda-client.cjs');
4
+ const require_sampling = require('./sampling.cjs');
4
5
  let uuid = require("uuid");
5
6
  let _segment_analytics_node = require("@segment/analytics-node");
6
7
 
@@ -20,6 +21,7 @@ var TelemetryClient = class {
20
21
  this.cloudConfiguration = null;
21
22
  this.licenseToken = null;
22
23
  this.telemetryId = null;
24
+ this.licenseTelemetryId = null;
23
25
  this.telemetryDisabled = false;
24
26
  this.sampleRate = .05;
25
27
  this.anonymousId = `anon_${(0, uuid.v4)()}`;
@@ -38,18 +40,28 @@ var TelemetryClient = class {
38
40
  return Math.random() < this.sampleRate;
39
41
  }
40
42
  async capture(event, properties) {
43
+ return this.captureWithIdentity(event, properties, {
44
+ telemetryId: this.telemetryId,
45
+ licenseToken: this.licenseToken,
46
+ licenseTelemetryId: this.licenseTelemetryId
47
+ });
48
+ }
49
+ async captureWithIdentity(event, properties, identity) {
41
50
  if (this.telemetryDisabled) return;
42
- if (!this.telemetryId && !this.shouldSendEvent()) return;
43
- const effectiveSampleRate = this.telemetryId ? 1 : this.sampleRate;
44
- const samplingMeta = {
45
- sampleRate: effectiveSampleRate,
46
- sampleRateAdjustmentFactor: 1 - effectiveSampleRate,
47
- sampleWeight: 1 / effectiveSampleRate
51
+ if (!identity.licenseTelemetryId && !this.shouldSendEvent()) return;
52
+ const eventMeta = {
53
+ ...require_sampling.computeSamplingMeta({
54
+ telemetryId: identity.licenseTelemetryId,
55
+ sampleRate: this.sampleRate
56
+ }),
57
+ telemetry_emitter: require_sampling.TELEMETRY_EMITTER_V1,
58
+ telemetry_event_id: (0, uuid.v4)()
48
59
  };
49
60
  const flattenedProperties = require_utils.flattenObject(properties);
50
61
  const propertiesWithGlobal = {
51
62
  ...this.globalProperties,
52
- ...samplingMeta,
63
+ ...eventMeta,
64
+ telemetry_transport: "segment",
53
65
  ...flattenedProperties
54
66
  };
55
67
  const orderedPropertiesWithGlobal = Object.keys(propertiesWithGlobal).sort().reduce((obj, key) => {
@@ -61,11 +73,13 @@ var TelemetryClient = class {
61
73
  properties: flattenedProperties,
62
74
  globalProperties: {
63
75
  ...this.globalProperties,
64
- ...samplingMeta
76
+ ...eventMeta,
77
+ telemetry_transport: "lambda"
65
78
  },
66
79
  packageName: this.packageName,
67
80
  packageVersion: this.packageVersion,
68
- licenseToken: this.licenseToken ?? void 0
81
+ telemetryId: identity.telemetryId ?? void 0,
82
+ licenseToken: identity.licenseToken ?? void 0
69
83
  });
70
84
  if (this.segment) this.segment.track({
71
85
  anonymousId: this.anonymousId,
@@ -87,9 +101,56 @@ var TelemetryClient = class {
87
101
  baseUrl: properties.baseUrl
88
102
  } });
89
103
  }
104
+ /**
105
+ * Atomically configure standalone, legacy, or anonymous telemetry identity.
106
+ *
107
+ * A standalone id takes transport precedence over a supplied legacy license
108
+ * token, but only a license-derived id grants sampling authority. Neither
109
+ * value is added to event properties.
110
+ *
111
+ * @param identity - One standalone id, one legacy license token, or neither.
112
+ */
113
+ setTelemetryIdentity(identity) {
114
+ const resolvedIdentity = this.resolveTelemetryIdentity(identity);
115
+ this.telemetryId = resolvedIdentity.telemetryId;
116
+ this.licenseToken = resolvedIdentity.licenseToken;
117
+ this.licenseTelemetryId = resolvedIdentity.licenseTelemetryId;
118
+ }
119
+ /**
120
+ * Configure legacy license-derived telemetry identity.
121
+ *
122
+ * @param licenseToken - License token whose telemetry claim identifies sends.
123
+ */
90
124
  setLicenseToken(licenseToken) {
91
- this.licenseToken = licenseToken;
92
- this.telemetryId = require_lambda_client.parseAndWarnTelemetryId(licenseToken);
125
+ this.setTelemetryIdentity({ licenseToken });
126
+ }
127
+ /**
128
+ * Create an immutable capture scope for one runtime.
129
+ *
130
+ * The scope shares this client's sinks, process-wide opt-out, global
131
+ * properties, and sampling settings, but snapshots transport identity and
132
+ * license-derived sampling authority. Constructing another runtime cannot
133
+ * rewrite an existing scope.
134
+ *
135
+ * @param identity - The runtime's construction-time telemetry identity.
136
+ * @returns A capture-only client bound to that identity.
137
+ */
138
+ createScope(identity) {
139
+ const resolvedIdentity = this.resolveTelemetryIdentity(identity);
140
+ return { capture: (event, properties) => this.captureWithIdentity(event, properties, resolvedIdentity) };
141
+ }
142
+ resolveTelemetryIdentity(identity) {
143
+ const telemetryId = require_lambda_client.firstNonBlankTelemetryId(identity.telemetryId);
144
+ if (telemetryId !== void 0) return {
145
+ telemetryId,
146
+ licenseToken: null,
147
+ licenseTelemetryId: null
148
+ };
149
+ return {
150
+ telemetryId: null,
151
+ licenseToken: identity.licenseToken ?? null,
152
+ licenseTelemetryId: identity.licenseToken ? require_lambda_client.parseAndWarnTelemetryId(identity.licenseToken) : null
153
+ };
93
154
  }
94
155
  setSampleRate(sampleRate) {
95
156
  let _sampleRate;