@introspection-sdk/introspection-browser 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts ADDED
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Introspection Client for Browser
3
+ * Provides an API for tracking events and feedback using OTLP Logs.
4
+ *
5
+ * Identity and gen_ai context can be managed via baggage (requires OpenTelemetry
6
+ * browser SDK with Zone.js for automatic propagation) or via client instance.
7
+ */
8
+
9
+ import { SeverityNumber, type LogAttributes } from "@opentelemetry/api-logs";
10
+ import {
11
+ LoggerProvider,
12
+ BatchLogRecordProcessor,
13
+ } from "@opentelemetry/sdk-logs";
14
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
15
+ import { propagation, context } from "@opentelemetry/api";
16
+ import { VERSION } from "./version.js";
17
+ import {
18
+ generateEventId,
19
+ type IntrospectionClientOptions,
20
+ type FeedbackOptions,
21
+ type UserTraits,
22
+ type GenAiContext,
23
+ type IdentityContext,
24
+ } from "./types.js";
25
+
26
+ const ANONYMOUS_ID_KEY = "introspection_anonymous_id";
27
+ const USER_ID_KEY = "introspection_user_id";
28
+ const TRAITS_KEY = "introspection_traits";
29
+
30
+ /**
31
+ * Generate a UUID-like anonymous ID
32
+ */
33
+ function generateAnonymousId(): string {
34
+ const segments = [];
35
+ for (let i = 0; i < 4; i++) {
36
+ segments.push(Math.random().toString(16).substring(2, 6));
37
+ }
38
+ return segments.join("-");
39
+ }
40
+
41
+ /**
42
+ * Simple logger that respects debug flag
43
+ */
44
+ class Logger {
45
+ constructor(private debug: boolean) {}
46
+
47
+ log(...args: unknown[]): void {
48
+ if (this.debug) {
49
+ console.log("[introspection-sdk]", ...args);
50
+ }
51
+ }
52
+
53
+ warn(...args: unknown[]): void {
54
+ console.warn("[introspection-sdk]", ...args);
55
+ }
56
+
57
+ error(...args: unknown[]): void {
58
+ console.error("[introspection-sdk]", ...args);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Introspection client for tracking events and feedback in the browser using OTLP Logs.
64
+ *
65
+ * Identity (user_id, anonymous_id) and gen_ai context (conversation_id, previous_response_id, agent)
66
+ * can be managed via client instance (persisted to localStorage) or via OpenTelemetry baggage
67
+ * for scoped context.
68
+ *
69
+ * Note: Automatic baggage propagation in browsers requires Zone.js from @opentelemetry/context-zone.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const client = new IntrospectionClient({
74
+ * token: "intro_xxx",
75
+ * debug: true,
76
+ * });
77
+ *
78
+ * // Set identity once (persisted to localStorage)
79
+ * client.identify("user_123", { email: "user@example.com" });
80
+ *
81
+ * // Simple feedback
82
+ * client.feedback("thumbs_up", { comments: "Great response!" });
83
+ *
84
+ * // With explicit gen_ai context
85
+ * client.feedback("thumbs_down", {
86
+ * conversationId: "conv_456",
87
+ * previousResponseId: "msg_123",
88
+ * comments: "Off topic",
89
+ * });
90
+ *
91
+ * // With scoped baggage context
92
+ * await client.withConversation("conv_456", undefined, async () => {
93
+ * client.feedback("thumbs_up"); // automatically includes conversation_id
94
+ * });
95
+ *
96
+ * // Track a custom event
97
+ * client.track("Button Clicked", { buttonId: "submit" });
98
+ * ```
99
+ */
100
+ export class IntrospectionClient {
101
+ private loggerProvider: LoggerProvider;
102
+ private otelLogger: ReturnType<LoggerProvider["getLogger"]>;
103
+ private userId: string | undefined;
104
+ private anonymousId: string;
105
+ private traits: Record<string, unknown> = {};
106
+ private logger: Logger;
107
+
108
+ /**
109
+ * @param options - Client configuration (token, service name, advanced settings).
110
+ */
111
+ constructor(options: IntrospectionClientOptions = {}) {
112
+ const token = options.token || "";
113
+ const advanced = options.advanced || {};
114
+ const baseUrl =
115
+ advanced.baseUrl ||
116
+ (typeof process !== "undefined" && process.env?.INTROSPECTION_BASE_URL) ||
117
+ "https://otel.introspection.dev";
118
+ this.logger = new Logger(advanced.debug ?? false);
119
+
120
+ // Load persisted state from localStorage
121
+ this.anonymousId = this.loadAnonymousId();
122
+ this.userId = this.loadUserId();
123
+ this.traits = this.loadTraits();
124
+
125
+ if (!token) {
126
+ this.logger.warn("No token provided. Events will not be sent.");
127
+ }
128
+
129
+ // Construct endpoint URL for logs
130
+ let endpoint: string;
131
+ if (baseUrl.endsWith("/v1/logs")) {
132
+ endpoint = baseUrl;
133
+ } else {
134
+ endpoint = `${baseUrl.replace(/\/$/, "")}/v1/logs`;
135
+ }
136
+
137
+ // Build headers
138
+ const headers: Record<string, string> = {
139
+ Authorization: `Bearer ${token}`,
140
+ };
141
+ if (advanced.additionalHeaders) {
142
+ Object.assign(headers, advanced.additionalHeaders);
143
+ }
144
+
145
+ // Create OTLP log exporter with HTTP/JSON
146
+ const exporter = new OTLPLogExporter({
147
+ url: endpoint,
148
+ headers,
149
+ });
150
+
151
+ // Create batch processor
152
+ const processor = new BatchLogRecordProcessor(exporter, {
153
+ maxQueueSize: advanced.maxBatchSize ?? 100,
154
+ scheduledDelayMillis: advanced.flushInterval ?? 5000,
155
+ });
156
+
157
+ // Create logger provider with processor
158
+ this.loggerProvider = new LoggerProvider({
159
+ processors: [processor],
160
+ });
161
+
162
+ // Get a logger instance
163
+ this.otelLogger = this.loggerProvider.getLogger(
164
+ "@introspection-sdk/introspection-browser",
165
+ VERSION,
166
+ );
167
+
168
+ // Flush on page unload
169
+ if (typeof window !== "undefined") {
170
+ window.addEventListener("beforeunload", () => {
171
+ this.loggerProvider.forceFlush();
172
+ });
173
+
174
+ document.addEventListener("visibilitychange", () => {
175
+ if (document.visibilityState === "hidden") {
176
+ this.loggerProvider.forceFlush();
177
+ }
178
+ });
179
+ }
180
+
181
+ this.logger.log(`Initialized with endpoint: ${endpoint}`);
182
+ }
183
+
184
+ /**
185
+ * Load anonymous ID from localStorage, or generate a new one
186
+ */
187
+ private loadAnonymousId(): string {
188
+ if (typeof localStorage !== "undefined") {
189
+ const stored = localStorage.getItem(ANONYMOUS_ID_KEY);
190
+ if (stored) {
191
+ return stored;
192
+ }
193
+ }
194
+ const newId = generateAnonymousId();
195
+ this.persistAnonymousId(newId);
196
+ return newId;
197
+ }
198
+
199
+ /**
200
+ * Persist anonymous ID to localStorage
201
+ */
202
+ private persistAnonymousId(id: string): void {
203
+ if (typeof localStorage !== "undefined") {
204
+ localStorage.setItem(ANONYMOUS_ID_KEY, id);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Load user ID from localStorage
210
+ */
211
+ private loadUserId(): string | undefined {
212
+ if (typeof localStorage !== "undefined") {
213
+ return localStorage.getItem(USER_ID_KEY) || undefined;
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ /**
219
+ * Persist user ID to localStorage
220
+ */
221
+ private persistUserId(id: string): void {
222
+ if (typeof localStorage !== "undefined") {
223
+ localStorage.setItem(USER_ID_KEY, id);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Load traits from localStorage
229
+ */
230
+ private loadTraits(): Record<string, unknown> {
231
+ if (typeof localStorage !== "undefined") {
232
+ const stored = localStorage.getItem(TRAITS_KEY);
233
+ if (stored) {
234
+ try {
235
+ return JSON.parse(stored);
236
+ } catch {
237
+ return {};
238
+ }
239
+ }
240
+ }
241
+ return {};
242
+ }
243
+
244
+ /**
245
+ * Persist traits to localStorage
246
+ */
247
+ private persistTraits(traits: Record<string, unknown>): void {
248
+ if (typeof localStorage !== "undefined") {
249
+ localStorage.setItem(TRAITS_KEY, JSON.stringify(traits));
250
+ }
251
+ }
252
+
253
+ /** Get gen_ai context from baggage */
254
+ private getGenAiFromContext(): GenAiContext {
255
+ const baggage = propagation.getBaggage(context.active());
256
+ return {
257
+ conversationId: baggage?.getEntry("gen_ai.conversation.id")?.value,
258
+ previousResponseId: baggage?.getEntry(
259
+ "gen_ai.request.previous_response_id",
260
+ )?.value,
261
+ agentName: baggage?.getEntry("gen_ai.agent.name")?.value,
262
+ agentId: baggage?.getEntry("gen_ai.agent.id")?.value,
263
+ };
264
+ }
265
+
266
+ /** Get identity from baggage or client instance */
267
+ private getIdentityFromContext(): IdentityContext {
268
+ const baggage = propagation.getBaggage(context.active());
269
+ return {
270
+ userId: baggage?.getEntry("identity.user_id")?.value || this.userId,
271
+ anonymousId:
272
+ baggage?.getEntry("identity.anonymous_id")?.value || this.anonymousId,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Get current timestamp as OpenTelemetry HrTime.
278
+ * Uses performance.now() for high-resolution timing in browsers.
279
+ */
280
+ private getTimestamp(): [number, number] {
281
+ const timeOrigin = performance.timeOrigin;
282
+ const now = performance.now();
283
+ const epochMs = timeOrigin + now;
284
+ const seconds = Math.floor(epochMs / 1000);
285
+ const nanos = Math.floor((epochMs % 1000) * 1_000_000);
286
+ return [seconds, nanos];
287
+ }
288
+
289
+ /**
290
+ * Build log record attributes.
291
+ */
292
+ private buildAttributes(
293
+ eventName: string,
294
+ options: {
295
+ properties?: Record<string, unknown>;
296
+ traits?: Record<string, unknown>;
297
+ conversationId?: string;
298
+ previousResponseId?: string;
299
+ eventId?: string;
300
+ } = {},
301
+ ): LogAttributes {
302
+ const { properties, traits, conversationId, previousResponseId, eventId } =
303
+ options;
304
+
305
+ // Core fields
306
+ const attributes: LogAttributes = {
307
+ "event.name": eventName,
308
+ "event.id": eventId || generateEventId(),
309
+ };
310
+
311
+ // Browser context
312
+ if (typeof window !== "undefined") {
313
+ attributes["context.page.path"] = window.location.pathname;
314
+ attributes["context.page.url"] = window.location.href;
315
+ attributes["context.page.title"] = document.title;
316
+ attributes["context.page.referrer"] = document.referrer;
317
+ attributes["context.page.search"] = window.location.search;
318
+ attributes["context.userAgent"] = navigator.userAgent;
319
+ attributes["context.locale"] = navigator.language;
320
+ attributes["context.timezone"] =
321
+ Intl.DateTimeFormat().resolvedOptions().timeZone;
322
+ attributes["context.screen.width"] = window.screen.width;
323
+ attributes["context.screen.height"] = window.screen.height;
324
+ attributes["context.screen.density"] = window.devicePixelRatio;
325
+ }
326
+
327
+ // Identity
328
+ const identity = this.getIdentityFromContext();
329
+ if (identity.userId) {
330
+ attributes["identity.user.id"] = identity.userId;
331
+ }
332
+ if (identity.anonymousId) {
333
+ attributes["identity.anonymous.id"] = identity.anonymousId;
334
+ }
335
+
336
+ // Gen AI context (explicit params override baggage)
337
+ const genAi = this.getGenAiFromContext();
338
+ const finalConversationId = conversationId || genAi.conversationId;
339
+ const finalPreviousResponseId =
340
+ previousResponseId || genAi.previousResponseId;
341
+
342
+ if (finalConversationId) {
343
+ attributes["gen_ai.conversation.id"] = finalConversationId;
344
+ }
345
+ if (finalPreviousResponseId) {
346
+ attributes["gen_ai.request.previous_response_id"] =
347
+ finalPreviousResponseId;
348
+ }
349
+ if (genAi.agentName) {
350
+ attributes["gen_ai.agent.name"] = genAi.agentName;
351
+ }
352
+ if (genAi.agentId) {
353
+ attributes["gen_ai.agent.id"] = genAi.agentId;
354
+ }
355
+
356
+ // Flatten properties with "properties." prefix
357
+ if (properties) {
358
+ for (const [key, value] of Object.entries(properties)) {
359
+ if (value != null) {
360
+ const attrKey = `properties.${key}`;
361
+ if (typeof value === "object") {
362
+ attributes[attrKey] = JSON.stringify(value);
363
+ } else if (
364
+ typeof value === "string" ||
365
+ typeof value === "number" ||
366
+ typeof value === "boolean"
367
+ ) {
368
+ attributes[attrKey] = value;
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // Flatten traits with "context.traits." prefix
375
+ if (traits) {
376
+ for (const [key, value] of Object.entries(traits)) {
377
+ if (value != null) {
378
+ const attrKey = `context.traits.${key}`;
379
+ if (typeof value === "object") {
380
+ attributes[attrKey] = JSON.stringify(value);
381
+ } else if (
382
+ typeof value === "string" ||
383
+ typeof value === "number" ||
384
+ typeof value === "boolean"
385
+ ) {
386
+ attributes[attrKey] = value;
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ return attributes;
393
+ }
394
+
395
+ /**
396
+ * Track a custom event.
397
+ *
398
+ * @param eventName - The event name (e.g., `"Button Clicked"`).
399
+ * @param properties - Optional properties to attach to the event.
400
+ * @param options - Optional overrides (e.g., custom `eventId`).
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * client.track("Button Clicked", { buttonId: "submit" });
405
+ * ```
406
+ */
407
+ track(
408
+ eventName: string,
409
+ properties?: Record<string, unknown>,
410
+ options?: { eventId?: string },
411
+ ): void {
412
+ const attributes = this.buildAttributes(eventName, {
413
+ properties,
414
+ eventId: options?.eventId,
415
+ });
416
+
417
+ this.otelLogger.emit({
418
+ timestamp: this.getTimestamp(),
419
+ context: context.active(),
420
+ severityNumber: SeverityNumber.INFO,
421
+ severityText: "INFO",
422
+ attributes,
423
+ });
424
+
425
+ this.logger.log(`Tracked: ${eventName}`);
426
+ }
427
+
428
+ /**
429
+ * Track feedback on a message or response.
430
+ *
431
+ * gen_ai.conversation.id, gen_ai.request.previous_response_id, and agent context
432
+ * are automatically extracted from baggage if not provided explicitly.
433
+ *
434
+ * @param name - Feedback name/action (e.g., "thumbs_up", "thumbs_down")
435
+ * @param options - Optional feedback options
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * // Simple feedback
440
+ * client.feedback("thumbs_up");
441
+ *
442
+ * // With comments
443
+ * client.feedback("thumbs_down", { comments: "Off topic" });
444
+ *
445
+ * // With explicit gen_ai context
446
+ * client.feedback("thumbs_up", {
447
+ * conversationId: "conv_456",
448
+ * previousResponseId: "msg_123",
449
+ * });
450
+ *
451
+ * // With baggage context
452
+ * await client.withConversation("conv_456", undefined, async () => {
453
+ * client.feedback("thumbs_up"); // auto-includes conversation_id
454
+ * });
455
+ * ```
456
+ */
457
+ feedback(name: string, options: FeedbackOptions = {}): void {
458
+ const { comments, conversationId, previousResponseId, eventId, ...extra } =
459
+ options;
460
+
461
+ const properties: Record<string, unknown> = { name, ...extra };
462
+ if (comments) {
463
+ properties.comments = comments;
464
+ }
465
+
466
+ const attributes = this.buildAttributes("introspection.feedback", {
467
+ properties,
468
+ conversationId: conversationId as string | undefined,
469
+ previousResponseId: previousResponseId as string | undefined,
470
+ eventId: eventId as string | undefined,
471
+ });
472
+
473
+ this.otelLogger.emit({
474
+ timestamp: this.getTimestamp(),
475
+ context: context.active(),
476
+ severityNumber: SeverityNumber.INFO,
477
+ severityText: "INFO",
478
+ attributes,
479
+ });
480
+
481
+ this.logger.log(`Feedback: ${name}`);
482
+ }
483
+
484
+ /**
485
+ * Identify a user and persist their traits to `localStorage`.
486
+ *
487
+ * @param userId - The user's unique identifier.
488
+ * @param traits - Optional user traits (email, name, plan, etc.).
489
+ * @param anonymousId - Optional anonymous ID to associate.
490
+ * @param eventId - Optional event ID (auto-generated if omitted).
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * client.identify("user-123", { email: "user@example.com", plan: "pro" });
495
+ * ```
496
+ */
497
+ identify(
498
+ userId: string,
499
+ traits?: UserTraits,
500
+ anonymousId?: string,
501
+ eventId?: string,
502
+ ): void {
503
+ // Store and persist
504
+ this.userId = userId;
505
+ this.persistUserId(userId);
506
+ if (anonymousId) {
507
+ this.anonymousId = anonymousId;
508
+ this.persistAnonymousId(anonymousId);
509
+ }
510
+ if (traits) {
511
+ this.traits = { ...this.traits, ...traits };
512
+ this.persistTraits(this.traits);
513
+ }
514
+
515
+ const attributes = this.buildAttributes("identify", { traits, eventId });
516
+
517
+ this.otelLogger.emit({
518
+ timestamp: this.getTimestamp(),
519
+ context: context.active(),
520
+ severityNumber: SeverityNumber.INFO,
521
+ severityText: "INFO",
522
+ attributes,
523
+ });
524
+
525
+ this.logger.log(`Identified: ${userId}`);
526
+ }
527
+
528
+ /**
529
+ * Set the authenticated user ID and persist it to `localStorage`.
530
+ *
531
+ * @param userId - The user's unique identifier.
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * client.setUserId("user-123");
536
+ * ```
537
+ */
538
+ setUserId(userId: string): void {
539
+ this.userId = userId;
540
+ this.persistUserId(userId);
541
+ }
542
+
543
+ /**
544
+ * Override the anonymous ID and persist it to `localStorage`.
545
+ *
546
+ * @param anonymousId - The anonymous visitor identifier.
547
+ *
548
+ * @example
549
+ * ```ts
550
+ * client.setAnonymousId("anon-abc");
551
+ * ```
552
+ */
553
+ setAnonymousId(anonymousId: string): void {
554
+ this.anonymousId = anonymousId;
555
+ this.persistAnonymousId(anonymousId);
556
+ }
557
+
558
+ /**
559
+ * Get the current anonymous visitor identifier.
560
+ *
561
+ * @returns The anonymous ID (auto-generated on first access if not set).
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * const anonId = client.getAnonymousId();
566
+ * ```
567
+ */
568
+ getAnonymousId(): string {
569
+ return this.anonymousId;
570
+ }
571
+
572
+ /**
573
+ * Create a context with baggage values set.
574
+ * Use with context.with() to run code in that context.
575
+ *
576
+ * Note: In browsers, automatic context propagation requires Zone.js.
577
+ * Without it, you must manually pass context through async operations.
578
+ *
579
+ * @param values - Key/value pairs to set as baggage
580
+ * @returns Context with baggage set
581
+ *
582
+ * @example
583
+ * ```typescript
584
+ * const ctx = client.createBaggageContext({
585
+ * "gen_ai.agent.name": "support-bot",
586
+ * "gen_ai.conversation.id": "conv_456"
587
+ * });
588
+ *
589
+ * await context.with(ctx, async () => {
590
+ * client.feedback("thumbs_up"); // picks up baggage
591
+ * });
592
+ * ```
593
+ */
594
+ createBaggageContext(values: Record<string, string>) {
595
+ const ctx = context.active();
596
+ let bag = propagation.getBaggage(ctx) || propagation.createBaggage();
597
+
598
+ for (const [key, value] of Object.entries(values)) {
599
+ bag = bag.setEntry(key, { value });
600
+ }
601
+
602
+ return propagation.setBaggage(ctx, bag);
603
+ }
604
+
605
+ /**
606
+ * Helper to set multiple baggage values and run a callback.
607
+ *
608
+ * @param values - Key/value pairs to set as baggage.
609
+ * @param callback - Function to run with baggage context.
610
+ * @returns The return value of the callback.
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * await client.withBaggage({
615
+ * "gen_ai.conversation.id": "conv_456",
616
+ * "gen_ai.request.previous_response_id": "resp_123"
617
+ * }, async () => {
618
+ * client.feedback("thumbs_up");
619
+ * });
620
+ * ```
621
+ */
622
+ async withBaggage<T>(
623
+ values: Record<string, string>,
624
+ callback: () => T | Promise<T>,
625
+ ): Promise<T> {
626
+ const ctx = this.createBaggageContext(values);
627
+ return await context.with(ctx, callback);
628
+ }
629
+
630
+ /**
631
+ * Helper to set agent baggage and run a callback.
632
+ *
633
+ * @param agentName - Name of the agent.
634
+ * @param agentId - Optional unique identifier for the agent.
635
+ * @param callback - Function to run with agent context.
636
+ * @returns The return value of the callback.
637
+ *
638
+ * @example
639
+ * ```typescript
640
+ * await client.withAgent("support-bot", "agent_123", async () => {
641
+ * client.feedback("thumbs_up");
642
+ * });
643
+ * ```
644
+ */
645
+ async withAgent<T>(
646
+ agentName: string,
647
+ agentId: string | undefined,
648
+ callback: () => T | Promise<T>,
649
+ ): Promise<T> {
650
+ const values: Record<string, string> = {
651
+ "gen_ai.agent.name": agentName,
652
+ };
653
+ if (agentId) {
654
+ values["gen_ai.agent.id"] = agentId;
655
+ }
656
+ const ctx = this.createBaggageContext(values);
657
+ return await context.with(ctx, callback);
658
+ }
659
+
660
+ /**
661
+ * Helper to set conversation baggage and run a callback.
662
+ *
663
+ * @param conversationId - Unique identifier for the conversation (optional).
664
+ * @param previousResponseId - Previous response ID for conversation continuity (optional).
665
+ * @param callback - Function to run with conversation context.
666
+ * @returns The return value of the callback.
667
+ *
668
+ * @example
669
+ * ```typescript
670
+ * // Simple conversation context
671
+ * await client.withConversation("conv_456", undefined, async () => {
672
+ * client.feedback("thumbs_up");
673
+ * });
674
+ *
675
+ * // With previous response for conversation continuity
676
+ * await client.withConversation("conv_456", "resp_123", async () => {
677
+ * client.feedback("thumbs_up");
678
+ * });
679
+ *
680
+ * // Just previousResponseId when already in a conversation
681
+ * await client.withConversation("conv_456", undefined, async () => {
682
+ * await client.withConversation(undefined, "resp_1", async () => {
683
+ * client.feedback("thumbs_up");
684
+ * });
685
+ * });
686
+ * ```
687
+ */
688
+ async withConversation<T>(
689
+ conversationId: string | undefined,
690
+ previousResponseId: string | undefined,
691
+ callback: () => T | Promise<T>,
692
+ ): Promise<T> {
693
+ const values: Record<string, string> = {};
694
+ if (conversationId) {
695
+ values["gen_ai.conversation.id"] = conversationId;
696
+ }
697
+ if (previousResponseId) {
698
+ values["gen_ai.request.previous_response_id"] = previousResponseId;
699
+ }
700
+ const ctx = this.createBaggageContext(values);
701
+ return await context.with(ctx, callback);
702
+ }
703
+
704
+ /**
705
+ * Helper to set user ID baggage and run a callback.
706
+ *
707
+ * @param userId - User identifier.
708
+ * @param callback - Function to run with user context.
709
+ * @returns The return value of the callback.
710
+ *
711
+ * @example
712
+ * ```typescript
713
+ * await client.withUserId("user_123", async () => {
714
+ * client.feedback("thumbs_up");
715
+ * });
716
+ * ```
717
+ */
718
+ async withUserId<T>(
719
+ userId: string,
720
+ callback: () => T | Promise<T>,
721
+ ): Promise<T> {
722
+ const ctx = this.createBaggageContext({
723
+ "identity.user_id": userId,
724
+ });
725
+ return await context.with(ctx, callback);
726
+ }
727
+
728
+ /**
729
+ * Helper to set anonymous ID baggage and run a callback.
730
+ *
731
+ * @param anonymousId - Anonymous visitor identifier.
732
+ * @param callback - Function to run with anonymous context.
733
+ * @returns The return value of the callback.
734
+ *
735
+ * @example
736
+ * ```ts
737
+ * await client.withAnonymousId("anon-abc", async () => {
738
+ * client.feedback("thumbs_up");
739
+ * });
740
+ * ```
741
+ */
742
+ async withAnonymousId<T>(
743
+ anonymousId: string,
744
+ callback: () => T | Promise<T>,
745
+ ): Promise<T> {
746
+ const ctx = this.createBaggageContext({
747
+ "identity.anonymous_id": anonymousId,
748
+ });
749
+ return await context.with(ctx, callback);
750
+ }
751
+
752
+ /**
753
+ * Reset the client state, clearing user identity and generating a new
754
+ * anonymous ID. Equivalent to a logout operation.
755
+ *
756
+ * @example
757
+ * ```ts
758
+ * client.reset();
759
+ * ```
760
+ */
761
+ reset(): void {
762
+ this.userId = undefined;
763
+ this.anonymousId = generateAnonymousId();
764
+ this.traits = {};
765
+
766
+ if (typeof localStorage !== "undefined") {
767
+ localStorage.removeItem(USER_ID_KEY);
768
+ localStorage.removeItem(TRAITS_KEY);
769
+ localStorage.setItem(ANONYMOUS_ID_KEY, this.anonymousId);
770
+ }
771
+
772
+ this.logger.log("Reset complete");
773
+ }
774
+
775
+ /**
776
+ * Flush all queued log records to the Introspection backend.
777
+ *
778
+ * @returns A promise that resolves once the flush completes.
779
+ */
780
+ async flush(): Promise<void> {
781
+ await this.loggerProvider.forceFlush();
782
+ this.logger.log("Flushed pending log records");
783
+ }
784
+
785
+ /**
786
+ * Shut down the client, flushing all pending log records.
787
+ *
788
+ * @returns A promise that resolves once shutdown is complete.
789
+ */
790
+ async shutdown(): Promise<void> {
791
+ await this.loggerProvider.shutdown();
792
+ this.logger.log("Shutdown complete");
793
+ }
794
+ }