@@ -2,9 +2,7 @@ import { Analytics } from "@segment/analytics-node";
22import type { AnalyticsEvents } from "./events" ;
33import { flattenObject } from "./utils" ;
44import { v4 as uuidv4 } from "uuid" ;
5- import lambdaClient , {
6- warnIfLicenseTokenLacksTelemetryId ,
7- } from "./lambda-client" ;
5+ import lambdaClient , { parseTelemetryIdFromLicense } from "./lambda-client" ;
86
97/**
108 * Checks if telemetry is disabled via environment variables.
@@ -32,12 +30,18 @@ export class TelemetryClient {
3230 // client decodes its payload to extract telemetry_id. Customer API
3331 // keys are NOT used here — they flow only into Segment.
3432 private licenseToken : string | null = null ;
33+ // Parsed telemetry_id from the license-token JWT payload. Cached at
34+ // setLicenseToken time so `capture()` can branch on identified vs
35+ // anonymous without re-parsing per event. Null when the token is
36+ // absent or yielded no telemetry_id.
37+ private telemetryId : string | null = null ;
3538 packageName : string ;
3639 packageVersion : string ;
3740 private telemetryDisabled : boolean = false ;
38- // Sample rate gates the Segment path only. The telemetry-sink Lambda
39- // handles sampling for the Scarf / Reo / future-vendor fan-out
40- // server-side, so the Lambda call always fires (subject to telemetryDisabled).
41+ // Client-side sampling rate for anonymous events. Identified events
42+ // (those whose license token yielded a telemetry_id) bypass the gate
43+ // entirely. Applied uniformly to both the lambda sink and Segment —
44+ // one dice roll per capture, both sinks see the same decision.
4145 private sampleRate : number = 0.05 ;
4246 private anonymousId = `anon_${ uuidv4 ( ) } ` ;
4347
@@ -92,6 +96,14 @@ export class TelemetryClient {
9296 return ;
9397 }
9498
99+ // Anonymous callers (no telemetry_id) are gated by sampleRate.
100+ // Identified callers (license token with telemetry_id) always send —
101+ // the volume is bounded by paying-customer count and full fidelity
102+ // per identified customer is worth the marginal cost.
103+ if ( ! this . telemetryId && ! this . shouldSendEvent ( ) ) {
104+ return ;
105+ }
106+
95107 const flattenedProperties = flattenObject ( properties ) ;
96108 const propertiesWithGlobal = {
97109 ...this . globalProperties ,
@@ -107,9 +119,6 @@ export class TelemetryClient {
107119 { } as Record < string , any > ,
108120 ) ;
109121
110- // Always send to the telemetry-sink Lambda — sampling happens
111- // server-side. The Lambda fans out to Scarf, Reo, and any future
112- // vendor sinks.
113122 await lambdaClient . send ( {
114123 event,
115124 properties : flattenedProperties ,
@@ -119,9 +128,7 @@ export class TelemetryClient {
119128 licenseToken : this . licenseToken ?? undefined ,
120129 } ) ;
121130
122- // Segment path retained for CopilotCloud-specific user analytics that
123- // pre-date the Lambda. Keeps its existing 5% client-side sampling.
124- if ( this . shouldSendEvent ( ) && this . segment ) {
131+ if ( this . segment ) {
125132 this . segment . track ( {
126133 anonymousId : this . anonymousId ,
127134 event,
@@ -154,7 +161,15 @@ export class TelemetryClient {
154161 // travels, in the X-CopilotKit-Telemetry-Id header set by lambda-client.
155162 setLicenseToken ( licenseToken : string ) {
156163 this . licenseToken = licenseToken ;
157- warnIfLicenseTokenLacksTelemetryId ( licenseToken ) ;
164+ this . telemetryId = parseTelemetryIdFromLicense ( licenseToken ) ;
165+ if ( ! this . telemetryId ) {
166+ // Smoke signal during the issuer rollout: a token was provided
167+ // but no telemetry_id came back. Surface it once at configuration
168+ // time rather than silently degrading to anonymous on every send.
169+ console . warn (
170+ "[CopilotKit] License token did not yield a telemetry_id; telemetry events will be sent anonymously." ,
171+ ) ;
172+ }
158173 }
159174
160175 private setSampleRate ( sampleRate : number | undefined ) {
0 commit comments