@shellapps/experience 1.0.1 → 1.2.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/dist/index.js CHANGED
@@ -20,461 +20,2594 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Experience: () => Experience
23
+ AutoTracker: () => AutoTracker,
24
+ BreadcrumbManager: () => BreadcrumbManager,
25
+ DataTTracker: () => DataTTracker,
26
+ ErrorCapture: () => ErrorCapture,
27
+ EventBatcher: () => EventBatcher,
28
+ Experience: () => Experience,
29
+ FeatureFlagClient: () => FeatureFlagClient,
30
+ HeatmapTracker: () => HeatmapTracker,
31
+ OfflineQueue: () => OfflineQueue,
32
+ ProtoSchemas: () => schemas_exports,
33
+ ProtobufEncoder: () => ProtobufEncoder,
34
+ SessionManager: () => SessionManager,
35
+ Severity: () => Severity,
36
+ Transport: () => Transport
24
37
  });
25
38
  module.exports = __toCommonJS(index_exports);
26
39
 
40
+ // src/types.ts
41
+ var Severity = /* @__PURE__ */ ((Severity2) => {
42
+ Severity2[Severity2["INFO"] = 0] = "INFO";
43
+ Severity2[Severity2["WARNING"] = 1] = "WARNING";
44
+ Severity2[Severity2["ERROR"] = 2] = "ERROR";
45
+ Severity2[Severity2["FATAL"] = 3] = "FATAL";
46
+ return Severity2;
47
+ })(Severity || {});
48
+
49
+ // src/session.ts
50
+ var SESSION_STORAGE_KEY = "shellapps_experience_session";
51
+ var SessionManager = class {
52
+ constructor() {
53
+ this.session = null;
54
+ this.initializeSession();
55
+ }
56
+ generateUUID() {
57
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
58
+ return crypto.randomUUID();
59
+ }
60
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
61
+ const r = Math.random() * 16 | 0;
62
+ const v = c === "x" ? r : r & 3 | 8;
63
+ return v.toString(16);
64
+ });
65
+ }
66
+ initializeSession() {
67
+ if (typeof sessionStorage !== "undefined") {
68
+ try {
69
+ const storedSession = sessionStorage.getItem(SESSION_STORAGE_KEY);
70
+ if (storedSession) {
71
+ const parsed = JSON.parse(storedSession);
72
+ this.session = {
73
+ id: parsed.id,
74
+ startTime: parsed.startTime,
75
+ getDuration: () => Date.now() - parsed.startTime
76
+ };
77
+ return;
78
+ }
79
+ } catch (error) {
80
+ }
81
+ }
82
+ this.createNewSession();
83
+ }
84
+ createNewSession() {
85
+ const startTime = Date.now();
86
+ const sessionId = this.generateUUID();
87
+ this.session = {
88
+ id: sessionId,
89
+ startTime,
90
+ getDuration: () => Date.now() - startTime
91
+ };
92
+ if (typeof sessionStorage !== "undefined") {
93
+ try {
94
+ sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify({
95
+ id: sessionId,
96
+ startTime
97
+ }));
98
+ } catch (error) {
99
+ }
100
+ }
101
+ }
102
+ getCurrentSession() {
103
+ if (!this.session) {
104
+ this.createNewSession();
105
+ }
106
+ return this.session;
107
+ }
108
+ getSessionId() {
109
+ return this.getCurrentSession().id;
110
+ }
111
+ getSessionDuration() {
112
+ return this.getCurrentSession().getDuration();
113
+ }
114
+ renewSession() {
115
+ this.createNewSession();
116
+ }
117
+ };
118
+
119
+ // src/proto/schemas.ts
120
+ var schemas_exports = {};
121
+ __export(schemas_exports, {
122
+ BreadcrumbCategoryEnum: () => BreadcrumbCategoryEnum,
123
+ BreadcrumbLevelEnum: () => BreadcrumbLevelEnum,
124
+ BreadcrumbSchema: () => BreadcrumbSchema,
125
+ BrowserContextSchema: () => BrowserContextSchema,
126
+ ElementContextSchema: () => ElementContextSchema,
127
+ ErrorBatchSchema: () => ErrorBatchSchema,
128
+ ErrorEventSchema: () => ErrorEventSchema,
129
+ EventBatchSchema: () => EventBatchSchema,
130
+ HeatmapEventSchema: () => HeatmapEventSchema,
131
+ HeatmapTypeEnum: () => HeatmapTypeEnum,
132
+ IngestResponseSchema: () => IngestResponseSchema,
133
+ PageContextSchema: () => PageContextSchema,
134
+ PositionSchema: () => PositionSchema,
135
+ SeverityEnum: () => SeverityEnum,
136
+ TrackEventSchema: () => TrackEventSchema,
137
+ ViewportSchema: () => ViewportSchema,
138
+ WireType: () => WireType
139
+ });
140
+ var TrackEventSchema = {
141
+ eventType: 1,
142
+ sessionId: 2,
143
+ timestamp: 3,
144
+ metadata: 4,
145
+ pageContext: 5,
146
+ elementContext: 6
147
+ };
148
+ var PageContextSchema = {
149
+ url: 1,
150
+ title: 2,
151
+ referrer: 3,
152
+ viewport: 4,
153
+ loadTime: 5
154
+ };
155
+ var ViewportSchema = {
156
+ width: 1,
157
+ height: 2
158
+ };
159
+ var ElementContextSchema = {
160
+ selector: 1,
161
+ dataT: 2,
162
+ tag: 3,
163
+ position: 4
164
+ };
165
+ var PositionSchema = {
166
+ x: 1,
167
+ y: 2
168
+ };
169
+ var HeatmapEventSchema = {
170
+ sessionId: 1,
171
+ timestamp: 2,
172
+ type: 3,
173
+ x: 4,
174
+ y: 5,
175
+ viewportWidth: 6,
176
+ viewportHeight: 7,
177
+ url: 8
178
+ };
179
+ var EventBatchSchema = {
180
+ events: 1,
181
+ heatmapEvents: 2
182
+ };
183
+ var IngestResponseSchema = {
184
+ success: 1,
185
+ message: 2
186
+ };
187
+ var ErrorEventSchema = {
188
+ sessionId: 1,
189
+ timestamp: 2,
190
+ message: 3,
191
+ filename: 4,
192
+ lineno: 5,
193
+ colno: 6,
194
+ stack: 7,
195
+ severity: 8,
196
+ breadcrumbs: 9,
197
+ browserContext: 10,
198
+ tags: 11,
199
+ extra: 12
200
+ };
201
+ var BreadcrumbSchema = {
202
+ timestamp: 1,
203
+ category: 2,
204
+ message: 3,
205
+ data: 4,
206
+ level: 5
207
+ };
208
+ var BrowserContextSchema = {
209
+ userAgent: 1,
210
+ url: 2,
211
+ timestamp: 3,
212
+ viewport: 4
213
+ };
214
+ var ErrorBatchSchema = {
215
+ errors: 1
216
+ };
217
+ var WireType = {
218
+ VARINT: 0,
219
+ FIXED64: 1,
220
+ LENGTH_DELIMITED: 2,
221
+ START_GROUP: 3,
222
+ END_GROUP: 4,
223
+ FIXED32: 5
224
+ };
225
+ var SeverityEnum = {
226
+ INFO: 0,
227
+ WARNING: 1,
228
+ ERROR: 2,
229
+ FATAL: 3
230
+ };
231
+ var HeatmapTypeEnum = {
232
+ click: 0,
233
+ move: 1,
234
+ hover: 2
235
+ };
236
+ var BreadcrumbCategoryEnum = {
237
+ navigation: 0,
238
+ click: 1,
239
+ console: 2,
240
+ fetch: 3,
241
+ xhr: 4,
242
+ ui: 5
243
+ };
244
+ var BreadcrumbLevelEnum = {
245
+ info: 0,
246
+ warning: 1,
247
+ error: 2
248
+ };
249
+
250
+ // src/proto/encode.ts
251
+ var ProtobufEncoder = class _ProtobufEncoder {
252
+ constructor() {
253
+ this.buffer = [];
254
+ }
255
+ // Helper methods for encoding
256
+ encodeVarint(value) {
257
+ while (value >= 128) {
258
+ this.buffer.push(value & 127 | 128);
259
+ value >>>= 7;
260
+ }
261
+ this.buffer.push(value & 127);
262
+ }
263
+ encodeTag(fieldNumber, wireType) {
264
+ this.encodeVarint(fieldNumber << 3 | wireType);
265
+ }
266
+ encodeString(value) {
267
+ const utf8Bytes = new TextEncoder().encode(value);
268
+ this.encodeVarint(utf8Bytes.length);
269
+ this.buffer.push(...utf8Bytes);
270
+ }
271
+ encodeUint64(value) {
272
+ this.encodeVarint(value);
273
+ }
274
+ encodeUint32(value) {
275
+ this.encodeVarint(value);
276
+ }
277
+ encodeBool(value) {
278
+ this.buffer.push(value ? 1 : 0);
279
+ }
280
+ encodeMessage(encoder) {
281
+ const startLength = this.buffer.length;
282
+ encoder();
283
+ const messageLength = this.buffer.length - startLength;
284
+ const lengthBytes = [];
285
+ let length = messageLength;
286
+ while (length >= 128) {
287
+ lengthBytes.push(length & 127 | 128);
288
+ length >>>= 7;
289
+ }
290
+ lengthBytes.push(length & 127);
291
+ this.buffer.splice(startLength, 0, ...lengthBytes);
292
+ return new Uint8Array(this.buffer.slice(startLength + lengthBytes.length));
293
+ }
294
+ encodeMapEntry(key, value) {
295
+ const entryEncoder = new _ProtobufEncoder();
296
+ entryEncoder.encodeTag(1, WireType.LENGTH_DELIMITED);
297
+ entryEncoder.encodeString(key);
298
+ entryEncoder.encodeTag(2, WireType.LENGTH_DELIMITED);
299
+ entryEncoder.encodeString(value);
300
+ this.encodeVarint(entryEncoder.buffer.length);
301
+ this.buffer.push(...entryEncoder.buffer);
302
+ }
303
+ // Encode viewport
304
+ encodeViewport(viewport) {
305
+ if (viewport.width) {
306
+ this.encodeTag(ViewportSchema.width, WireType.VARINT);
307
+ this.encodeUint32(viewport.width);
308
+ }
309
+ if (viewport.height) {
310
+ this.encodeTag(ViewportSchema.height, WireType.VARINT);
311
+ this.encodeUint32(viewport.height);
312
+ }
313
+ }
314
+ // Encode position
315
+ encodePosition(position) {
316
+ this.encodeTag(PositionSchema.x, WireType.VARINT);
317
+ this.encodeUint32(position.x);
318
+ this.encodeTag(PositionSchema.y, WireType.VARINT);
319
+ this.encodeUint32(position.y);
320
+ }
321
+ // Encode PageContext
322
+ encodePageContext(pageContext) {
323
+ this.encodeTag(PageContextSchema.url, WireType.LENGTH_DELIMITED);
324
+ this.encodeString(pageContext.url);
325
+ this.encodeTag(PageContextSchema.title, WireType.LENGTH_DELIMITED);
326
+ this.encodeString(pageContext.title);
327
+ this.encodeTag(PageContextSchema.referrer, WireType.LENGTH_DELIMITED);
328
+ this.encodeString(pageContext.referrer);
329
+ if (pageContext.viewport) {
330
+ this.encodeTag(PageContextSchema.viewport, WireType.LENGTH_DELIMITED);
331
+ const viewportEncoder = new _ProtobufEncoder();
332
+ viewportEncoder.encodeViewport(pageContext.viewport);
333
+ this.encodeVarint(viewportEncoder.buffer.length);
334
+ this.buffer.push(...viewportEncoder.buffer);
335
+ }
336
+ if (pageContext.loadTime) {
337
+ this.encodeTag(PageContextSchema.loadTime, WireType.VARINT);
338
+ this.encodeUint64(pageContext.loadTime);
339
+ }
340
+ }
341
+ // Encode ElementContext
342
+ encodeElementContext(elementContext) {
343
+ this.encodeTag(ElementContextSchema.selector, WireType.LENGTH_DELIMITED);
344
+ this.encodeString(elementContext.selector);
345
+ if (elementContext.dataT) {
346
+ this.encodeTag(ElementContextSchema.dataT, WireType.LENGTH_DELIMITED);
347
+ this.encodeString(elementContext.dataT);
348
+ }
349
+ this.encodeTag(ElementContextSchema.tag, WireType.LENGTH_DELIMITED);
350
+ this.encodeString(elementContext.tag);
351
+ if (elementContext.position) {
352
+ this.encodeTag(ElementContextSchema.position, WireType.LENGTH_DELIMITED);
353
+ const positionEncoder = new _ProtobufEncoder();
354
+ positionEncoder.encodePosition(elementContext.position);
355
+ this.encodeVarint(positionEncoder.buffer.length);
356
+ this.buffer.push(...positionEncoder.buffer);
357
+ }
358
+ }
359
+ // Encode TrackEvent
360
+ encodeTrackEvent(event) {
361
+ this.encodeTag(TrackEventSchema.eventType, WireType.LENGTH_DELIMITED);
362
+ this.encodeString(event.eventType);
363
+ this.encodeTag(TrackEventSchema.sessionId, WireType.LENGTH_DELIMITED);
364
+ this.encodeString(event.sessionId);
365
+ this.encodeTag(TrackEventSchema.timestamp, WireType.VARINT);
366
+ this.encodeUint64(event.timestamp);
367
+ if (event.metadata) {
368
+ for (const [key, value] of Object.entries(event.metadata)) {
369
+ this.encodeTag(TrackEventSchema.metadata, WireType.LENGTH_DELIMITED);
370
+ this.encodeMapEntry(key, value);
371
+ }
372
+ }
373
+ if (event.pageContext) {
374
+ this.encodeTag(TrackEventSchema.pageContext, WireType.LENGTH_DELIMITED);
375
+ const pageContextEncoder = new _ProtobufEncoder();
376
+ pageContextEncoder.encodePageContext(event.pageContext);
377
+ this.encodeVarint(pageContextEncoder.buffer.length);
378
+ this.buffer.push(...pageContextEncoder.buffer);
379
+ }
380
+ if (event.elementContext) {
381
+ this.encodeTag(TrackEventSchema.elementContext, WireType.LENGTH_DELIMITED);
382
+ const elementContextEncoder = new _ProtobufEncoder();
383
+ elementContextEncoder.encodeElementContext(event.elementContext);
384
+ this.encodeVarint(elementContextEncoder.buffer.length);
385
+ this.buffer.push(...elementContextEncoder.buffer);
386
+ }
387
+ }
388
+ // Encode HeatmapEvent
389
+ encodeHeatmapEvent(event) {
390
+ this.encodeTag(HeatmapEventSchema.sessionId, WireType.LENGTH_DELIMITED);
391
+ this.encodeString(event.sessionId);
392
+ this.encodeTag(HeatmapEventSchema.timestamp, WireType.VARINT);
393
+ this.encodeUint64(event.timestamp);
394
+ this.encodeTag(HeatmapEventSchema.type, WireType.VARINT);
395
+ this.encodeUint32(HeatmapTypeEnum[event.type]);
396
+ this.encodeTag(HeatmapEventSchema.x, WireType.VARINT);
397
+ this.encodeUint32(event.x);
398
+ this.encodeTag(HeatmapEventSchema.y, WireType.VARINT);
399
+ this.encodeUint32(event.y);
400
+ this.encodeTag(HeatmapEventSchema.viewportWidth, WireType.VARINT);
401
+ this.encodeUint32(event.viewportWidth);
402
+ this.encodeTag(HeatmapEventSchema.viewportHeight, WireType.VARINT);
403
+ this.encodeUint32(event.viewportHeight);
404
+ this.encodeTag(HeatmapEventSchema.url, WireType.LENGTH_DELIMITED);
405
+ this.encodeString(event.url);
406
+ }
407
+ // Encode Breadcrumb
408
+ encodeBreadcrumb(breadcrumb) {
409
+ this.encodeTag(BreadcrumbSchema.timestamp, WireType.VARINT);
410
+ this.encodeUint64(breadcrumb.timestamp);
411
+ this.encodeTag(BreadcrumbSchema.category, WireType.VARINT);
412
+ this.encodeUint32(BreadcrumbCategoryEnum[breadcrumb.category]);
413
+ this.encodeTag(BreadcrumbSchema.message, WireType.LENGTH_DELIMITED);
414
+ this.encodeString(breadcrumb.message);
415
+ if (breadcrumb.data) {
416
+ for (const [key, value] of Object.entries(breadcrumb.data)) {
417
+ this.encodeTag(BreadcrumbSchema.data, WireType.LENGTH_DELIMITED);
418
+ this.encodeMapEntry(key, JSON.stringify(value));
419
+ }
420
+ }
421
+ if (breadcrumb.level) {
422
+ this.encodeTag(BreadcrumbSchema.level, WireType.VARINT);
423
+ this.encodeUint32(BreadcrumbLevelEnum[breadcrumb.level]);
424
+ }
425
+ }
426
+ // Encode BrowserContext
427
+ encodeBrowserContext(context) {
428
+ this.encodeTag(BrowserContextSchema.userAgent, WireType.LENGTH_DELIMITED);
429
+ this.encodeString(context.userAgent);
430
+ this.encodeTag(BrowserContextSchema.url, WireType.LENGTH_DELIMITED);
431
+ this.encodeString(context.url);
432
+ this.encodeTag(BrowserContextSchema.timestamp, WireType.VARINT);
433
+ this.encodeUint64(context.timestamp);
434
+ this.encodeTag(BrowserContextSchema.viewport, WireType.LENGTH_DELIMITED);
435
+ const viewportEncoder = new _ProtobufEncoder();
436
+ viewportEncoder.encodeViewport(context.viewport);
437
+ this.encodeVarint(viewportEncoder.buffer.length);
438
+ this.buffer.push(...viewportEncoder.buffer);
439
+ }
440
+ // Encode ErrorEvent
441
+ encodeErrorEvent(error) {
442
+ this.encodeTag(ErrorEventSchema.sessionId, WireType.LENGTH_DELIMITED);
443
+ this.encodeString(error.sessionId);
444
+ this.encodeTag(ErrorEventSchema.timestamp, WireType.VARINT);
445
+ this.encodeUint64(error.timestamp);
446
+ this.encodeTag(ErrorEventSchema.message, WireType.LENGTH_DELIMITED);
447
+ this.encodeString(error.message);
448
+ if (error.filename) {
449
+ this.encodeTag(ErrorEventSchema.filename, WireType.LENGTH_DELIMITED);
450
+ this.encodeString(error.filename);
451
+ }
452
+ if (error.lineno) {
453
+ this.encodeTag(ErrorEventSchema.lineno, WireType.VARINT);
454
+ this.encodeUint32(error.lineno);
455
+ }
456
+ if (error.colno) {
457
+ this.encodeTag(ErrorEventSchema.colno, WireType.VARINT);
458
+ this.encodeUint32(error.colno);
459
+ }
460
+ if (error.stack) {
461
+ this.encodeTag(ErrorEventSchema.stack, WireType.LENGTH_DELIMITED);
462
+ this.encodeString(error.stack);
463
+ }
464
+ this.encodeTag(ErrorEventSchema.severity, WireType.VARINT);
465
+ this.encodeUint32(error.severity);
466
+ for (const breadcrumb of error.breadcrumbs) {
467
+ this.encodeTag(ErrorEventSchema.breadcrumbs, WireType.LENGTH_DELIMITED);
468
+ const breadcrumbEncoder = new _ProtobufEncoder();
469
+ breadcrumbEncoder.encodeBreadcrumb(breadcrumb);
470
+ this.encodeVarint(breadcrumbEncoder.buffer.length);
471
+ this.buffer.push(...breadcrumbEncoder.buffer);
472
+ }
473
+ this.encodeTag(ErrorEventSchema.browserContext, WireType.LENGTH_DELIMITED);
474
+ const browserContextEncoder = new _ProtobufEncoder();
475
+ browserContextEncoder.encodeBrowserContext(error.browserContext);
476
+ this.encodeVarint(browserContextEncoder.buffer.length);
477
+ this.buffer.push(...browserContextEncoder.buffer);
478
+ if (error.tags) {
479
+ for (const [key, value] of Object.entries(error.tags)) {
480
+ this.encodeTag(ErrorEventSchema.tags, WireType.LENGTH_DELIMITED);
481
+ this.encodeMapEntry(key, value);
482
+ }
483
+ }
484
+ if (error.extra) {
485
+ for (const [key, value] of Object.entries(error.extra)) {
486
+ this.encodeTag(ErrorEventSchema.extra, WireType.LENGTH_DELIMITED);
487
+ this.encodeMapEntry(key, JSON.stringify(value));
488
+ }
489
+ }
490
+ }
491
+ // Public encoding methods
492
+ encodeEventBatch(batch) {
493
+ this.buffer = [];
494
+ for (const event of batch.events) {
495
+ this.encodeTag(EventBatchSchema.events, WireType.LENGTH_DELIMITED);
496
+ const eventEncoder = new _ProtobufEncoder();
497
+ eventEncoder.encodeTrackEvent(event);
498
+ this.encodeVarint(eventEncoder.buffer.length);
499
+ this.buffer.push(...eventEncoder.buffer);
500
+ }
501
+ for (const event of batch.heatmapEvents) {
502
+ this.encodeTag(EventBatchSchema.heatmapEvents, WireType.LENGTH_DELIMITED);
503
+ const eventEncoder = new _ProtobufEncoder();
504
+ eventEncoder.encodeHeatmapEvent(event);
505
+ this.encodeVarint(eventEncoder.buffer.length);
506
+ this.buffer.push(...eventEncoder.buffer);
507
+ }
508
+ return new Uint8Array(this.buffer);
509
+ }
510
+ encodeErrorBatch(batch) {
511
+ this.buffer = [];
512
+ for (const error of batch.errors) {
513
+ this.encodeTag(ErrorBatchSchema.errors, WireType.LENGTH_DELIMITED);
514
+ const errorEncoder = new _ProtobufEncoder();
515
+ errorEncoder.encodeErrorEvent(error);
516
+ this.encodeVarint(errorEncoder.buffer.length);
517
+ this.buffer.push(...errorEncoder.buffer);
518
+ }
519
+ return new Uint8Array(this.buffer);
520
+ }
521
+ };
522
+
523
+ // src/transport.ts
524
+ var Transport = class {
525
+ constructor(config) {
526
+ this.config = config;
527
+ this.encoder = new ProtobufEncoder();
528
+ }
529
+ async sendProtobuf(endpoint, data) {
530
+ try {
531
+ const response = await fetch(`${this.config.endpoint}${endpoint}`, {
532
+ method: "POST",
533
+ headers: {
534
+ "Content-Type": "application/x-protobuf",
535
+ "X-API-Key": this.config.apiKey
536
+ },
537
+ body: data
538
+ });
539
+ if (!response.ok) {
540
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
541
+ }
542
+ try {
543
+ const result = await response.json();
544
+ return result;
545
+ } catch {
546
+ return { success: true };
547
+ }
548
+ } catch (error) {
549
+ if (this.config.debug) {
550
+ console.error("[ExperienceSDK] Protobuf transport error:", error);
551
+ }
552
+ throw error;
553
+ }
554
+ }
555
+ async sendJSON(endpoint, data) {
556
+ try {
557
+ const response = await fetch(`${this.config.endpoint}${endpoint}`, {
558
+ method: "POST",
559
+ headers: {
560
+ "Content-Type": "application/json",
561
+ "X-API-Key": this.config.apiKey
562
+ },
563
+ body: JSON.stringify(data)
564
+ });
565
+ if (!response.ok) {
566
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
567
+ }
568
+ try {
569
+ const result = await response.json();
570
+ return result;
571
+ } catch {
572
+ return { success: true };
573
+ }
574
+ } catch (error) {
575
+ if (this.config.debug) {
576
+ console.error("[ExperienceSDK] JSON transport error:", error);
577
+ }
578
+ throw error;
579
+ }
580
+ }
581
+ async sendEventBatch(batch) {
582
+ try {
583
+ if (!this.config.debug) {
584
+ const protobufData = this.encoder.encodeEventBatch(batch);
585
+ return await this.sendProtobuf("/api/v1/events/ingest", protobufData);
586
+ }
587
+ } catch (error) {
588
+ if (this.config.debug) {
589
+ console.warn("[ExperienceSDK] Protobuf failed, falling back to JSON:", error);
590
+ }
591
+ }
592
+ return this.sendJSON("/api/v1/events/ingest/json", batch);
593
+ }
594
+ async sendErrorBatch(batch) {
595
+ try {
596
+ if (!this.config.debug) {
597
+ const protobufData = this.encoder.encodeErrorBatch(batch);
598
+ return await this.sendProtobuf("/api/v1/errors/ingest", protobufData);
599
+ }
600
+ } catch (error) {
601
+ if (this.config.debug) {
602
+ console.warn("[ExperienceSDK] Protobuf failed, falling back to JSON:", error);
603
+ }
604
+ }
605
+ return this.sendJSON("/api/v1/errors/ingest/json", batch);
606
+ }
607
+ async sendEventBatchBeacon(batch) {
608
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
609
+ try {
610
+ const protobufData = this.encoder.encodeEventBatch(batch);
611
+ const blob = new Blob([protobufData], { type: "application/x-protobuf" });
612
+ const url = `${this.config.endpoint}/api/v1/events/ingest?api_key=${encodeURIComponent(this.config.apiKey)}`;
613
+ navigator.sendBeacon(url, blob);
614
+ } catch (error) {
615
+ if (this.config.debug) {
616
+ console.error("[ExperienceSDK] Beacon send error:", error);
617
+ }
618
+ try {
619
+ const jsonBlob = new Blob([JSON.stringify(batch)], { type: "application/json" });
620
+ const jsonUrl = `${this.config.endpoint}/api/v1/events/ingest/json?api_key=${encodeURIComponent(this.config.apiKey)}`;
621
+ navigator.sendBeacon(jsonUrl, jsonBlob);
622
+ } catch (fallbackError) {
623
+ }
624
+ }
625
+ }
626
+ }
627
+ async sendErrorBatchBeacon(batch) {
628
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
629
+ try {
630
+ const protobufData = this.encoder.encodeErrorBatch(batch);
631
+ const blob = new Blob([protobufData], { type: "application/x-protobuf" });
632
+ const url = `${this.config.endpoint}/api/v1/errors/ingest?api_key=${encodeURIComponent(this.config.apiKey)}`;
633
+ navigator.sendBeacon(url, blob);
634
+ } catch (error) {
635
+ if (this.config.debug) {
636
+ console.error("[ExperienceSDK] Error beacon send error:", error);
637
+ }
638
+ try {
639
+ const jsonBlob = new Blob([JSON.stringify(batch)], { type: "application/json" });
640
+ const jsonUrl = `${this.config.endpoint}/api/v1/errors/ingest/json?api_key=${encodeURIComponent(this.config.apiKey)}`;
641
+ navigator.sendBeacon(jsonUrl, jsonBlob);
642
+ } catch (fallbackError) {
643
+ }
644
+ }
645
+ }
646
+ }
647
+ async fetchFlags(appId, profileId) {
648
+ try {
649
+ const queryParams = new URLSearchParams();
650
+ if (profileId) {
651
+ queryParams.set("profileId", profileId);
652
+ }
653
+ const url = `${this.config.endpoint}/api/v1/flags/${appId}/resolve${queryParams.toString() ? "?" + queryParams.toString() : ""}`;
654
+ const response = await fetch(url, {
655
+ method: "GET",
656
+ headers: {
657
+ "X-API-Key": this.config.apiKey
658
+ }
659
+ });
660
+ if (!response.ok) {
661
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
662
+ }
663
+ const result = await response.json();
664
+ return result.flags || {};
665
+ } catch (error) {
666
+ if (this.config.debug) {
667
+ console.error("[ExperienceSDK] Flag fetch error:", error);
668
+ }
669
+ return {};
670
+ }
671
+ }
672
+ };
673
+
674
+ // src/batch.ts
675
+ var EventBatcher = class {
676
+ constructor(transport, batchIntervalMs = 2e3, maxBatchSize = 50, debug = false) {
677
+ this.trackEvents = [];
678
+ this.heatmapEvents = [];
679
+ this.errorEvents = [];
680
+ this.flushTimer = null;
681
+ this.unsubscribeFns = [];
682
+ this.transport = transport;
683
+ this.batchIntervalMs = batchIntervalMs;
684
+ this.maxBatchSize = maxBatchSize;
685
+ this.debug = debug;
686
+ this.setupAutoFlush();
687
+ this.setupUnloadHandler();
688
+ }
689
+ setupAutoFlush() {
690
+ this.scheduleNextFlush();
691
+ }
692
+ scheduleNextFlush() {
693
+ if (this.flushTimer !== null) {
694
+ clearTimeout(this.flushTimer);
695
+ }
696
+ this.flushTimer = window.setTimeout(() => {
697
+ this.flush();
698
+ this.scheduleNextFlush();
699
+ }, this.batchIntervalMs);
700
+ }
701
+ setupUnloadHandler() {
702
+ if (typeof window === "undefined") return;
703
+ const handleUnload = () => {
704
+ this.flushBeacon();
705
+ };
706
+ window.addEventListener("beforeunload", handleUnload);
707
+ window.addEventListener("unload", handleUnload);
708
+ window.addEventListener("pagehide", handleUnload);
709
+ if (typeof document !== "undefined") {
710
+ const handleVisibilityChange = () => {
711
+ if (document.visibilityState === "hidden") {
712
+ this.flushBeacon();
713
+ }
714
+ };
715
+ document.addEventListener("visibilitychange", handleVisibilityChange);
716
+ this.unsubscribeFns.push(() => {
717
+ window.removeEventListener("beforeunload", handleUnload);
718
+ window.removeEventListener("unload", handleUnload);
719
+ window.removeEventListener("pagehide", handleUnload);
720
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
721
+ });
722
+ } else {
723
+ this.unsubscribeFns.push(() => {
724
+ window.removeEventListener("beforeunload", handleUnload);
725
+ window.removeEventListener("unload", handleUnload);
726
+ window.removeEventListener("pagehide", handleUnload);
727
+ });
728
+ }
729
+ }
730
+ addTrackEvent(event) {
731
+ this.trackEvents.push(event);
732
+ if (this.debug) {
733
+ console.log("[ExperienceSDK] Track event added:", event);
734
+ }
735
+ if (this.trackEvents.length + this.heatmapEvents.length >= this.maxBatchSize) {
736
+ this.flush();
737
+ }
738
+ }
739
+ addHeatmapEvent(event) {
740
+ this.heatmapEvents.push(event);
741
+ if (this.debug) {
742
+ console.log("[ExperienceSDK] Heatmap event added:", event);
743
+ }
744
+ if (this.trackEvents.length + this.heatmapEvents.length >= this.maxBatchSize) {
745
+ this.flush();
746
+ }
747
+ }
748
+ addErrorEvent(error) {
749
+ this.errorEvents.push(error);
750
+ if (this.debug) {
751
+ console.log("[ExperienceSDK] Error event added:", error);
752
+ }
753
+ if (error.severity >= 2) {
754
+ this.flushErrors();
755
+ } else if (this.errorEvents.length >= this.maxBatchSize) {
756
+ this.flushErrors();
757
+ }
758
+ }
759
+ async flush() {
760
+ if (this.trackEvents.length === 0 && this.heatmapEvents.length === 0) {
761
+ return;
762
+ }
763
+ const batch = {
764
+ events: [...this.trackEvents],
765
+ heatmapEvents: [...this.heatmapEvents]
766
+ };
767
+ this.trackEvents = [];
768
+ this.heatmapEvents = [];
769
+ try {
770
+ const result = await this.transport.sendEventBatch(batch);
771
+ if (this.debug) {
772
+ console.log("[ExperienceSDK] Event batch sent successfully:", result);
773
+ }
774
+ } catch (error) {
775
+ if (this.debug) {
776
+ console.error("[ExperienceSDK] Failed to send event batch:", error);
777
+ }
778
+ this.trackEvents.unshift(...batch.events);
779
+ this.heatmapEvents.unshift(...batch.heatmapEvents);
780
+ this.trackEvents = this.trackEvents.slice(0, this.maxBatchSize * 2);
781
+ this.heatmapEvents = this.heatmapEvents.slice(0, this.maxBatchSize * 2);
782
+ }
783
+ }
784
+ async flushErrors() {
785
+ if (this.errorEvents.length === 0) {
786
+ return;
787
+ }
788
+ const batch = {
789
+ errors: [...this.errorEvents]
790
+ };
791
+ this.errorEvents = [];
792
+ try {
793
+ const result = await this.transport.sendErrorBatch(batch);
794
+ if (this.debug) {
795
+ console.log("[ExperienceSDK] Error batch sent successfully:", result);
796
+ }
797
+ } catch (error) {
798
+ if (this.debug) {
799
+ console.error("[ExperienceSDK] Failed to send error batch:", error);
800
+ }
801
+ this.errorEvents.unshift(...batch.errors);
802
+ this.errorEvents = this.errorEvents.slice(0, this.maxBatchSize * 2);
803
+ }
804
+ }
805
+ flushBeacon() {
806
+ if (this.trackEvents.length > 0 || this.heatmapEvents.length > 0) {
807
+ const eventBatch = {
808
+ events: [...this.trackEvents],
809
+ heatmapEvents: [...this.heatmapEvents]
810
+ };
811
+ this.transport.sendEventBatchBeacon(eventBatch);
812
+ this.trackEvents = [];
813
+ this.heatmapEvents = [];
814
+ }
815
+ if (this.errorEvents.length > 0) {
816
+ const errorBatch = {
817
+ errors: [...this.errorEvents]
818
+ };
819
+ this.transport.sendErrorBatchBeacon(errorBatch);
820
+ this.errorEvents = [];
821
+ }
822
+ }
823
+ getPendingEventCount() {
824
+ return this.trackEvents.length + this.heatmapEvents.length;
825
+ }
826
+ getPendingErrorCount() {
827
+ return this.errorEvents.length;
828
+ }
829
+ async shutdown() {
830
+ if (this.flushTimer !== null) {
831
+ clearTimeout(this.flushTimer);
832
+ this.flushTimer = null;
833
+ }
834
+ this.unsubscribeFns.forEach((fn) => fn());
835
+ this.unsubscribeFns = [];
836
+ await Promise.all([
837
+ this.flush(),
838
+ this.flushErrors()
839
+ ]);
840
+ }
841
+ // Force immediate flush for testing/debugging
842
+ async forceFlush() {
843
+ await Promise.all([
844
+ this.flush(),
845
+ this.flushErrors()
846
+ ]);
847
+ }
848
+ };
849
+
27
850
  // src/breadcrumbs.ts
28
851
  var MAX_BREADCRUMBS = 50;
29
852
  var BreadcrumbManager = class {
30
853
  constructor() {
31
- this.buffer = [];
854
+ this.breadcrumbs = [];
855
+ this.unsubscribeFns = [];
856
+ this.originalConsole = null;
32
857
  this.originalFetch = null;
33
- this.originalXhrOpen = null;
34
- this.clickHandler = null;
858
+ this.isEnabled = false;
859
+ }
860
+ enable() {
861
+ if (this.isEnabled) return;
862
+ this.isEnabled = true;
863
+ this.setupConsoleWrapping();
864
+ this.setupFetchWrapping();
865
+ this.setupXHRWrapping();
866
+ this.setupNavigationListeners();
867
+ this.setupClickListener();
868
+ }
869
+ disable() {
870
+ if (!this.isEnabled) return;
871
+ this.isEnabled = false;
872
+ this.unsubscribeFns.forEach((fn) => fn());
873
+ this.unsubscribeFns = [];
874
+ this.restoreConsole();
875
+ this.restoreFetch();
876
+ }
877
+ addBreadcrumb(breadcrumb) {
878
+ const fullBreadcrumb = {
879
+ ...breadcrumb,
880
+ timestamp: Date.now()
881
+ };
882
+ this.breadcrumbs.push(fullBreadcrumb);
883
+ if (this.breadcrumbs.length > MAX_BREADCRUMBS) {
884
+ this.breadcrumbs = this.breadcrumbs.slice(-MAX_BREADCRUMBS);
885
+ }
886
+ }
887
+ setupConsoleWrapping() {
888
+ if (typeof console === "undefined") return;
35
889
  this.originalConsole = {
36
- log: console.log,
37
890
  warn: console.warn,
38
891
  error: console.error
39
892
  };
893
+ console.warn = (...args) => {
894
+ this.addBreadcrumb({
895
+ category: "console",
896
+ message: args.map((arg) => String(arg)).join(" "),
897
+ level: "warning"
898
+ });
899
+ this.originalConsole.warn.apply(console, args);
900
+ };
901
+ console.error = (...args) => {
902
+ this.addBreadcrumb({
903
+ category: "console",
904
+ message: args.map((arg) => String(arg)).join(" "),
905
+ level: "error"
906
+ });
907
+ this.originalConsole.error.apply(console, args);
908
+ };
909
+ }
910
+ restoreConsole() {
911
+ if (this.originalConsole && typeof console !== "undefined") {
912
+ console.warn = this.originalConsole.warn;
913
+ console.error = this.originalConsole.error;
914
+ this.originalConsole = null;
915
+ }
916
+ }
917
+ setupFetchWrapping() {
918
+ if (typeof fetch === "undefined" || typeof window === "undefined") return;
919
+ this.originalFetch = window.fetch;
920
+ window.fetch = async (input, init) => {
921
+ const url = typeof input === "string" ? input : input.toString();
922
+ const method = init?.method || "GET";
923
+ const startTime = Date.now();
924
+ try {
925
+ const response = await this.originalFetch(input, init);
926
+ const duration = Date.now() - startTime;
927
+ this.addBreadcrumb({
928
+ category: "fetch",
929
+ message: `${method} ${url}`,
930
+ data: {
931
+ url,
932
+ method,
933
+ status: response.status,
934
+ duration: `${duration}ms`
935
+ },
936
+ level: response.ok ? "info" : "error"
937
+ });
938
+ return response;
939
+ } catch (error) {
940
+ const duration = Date.now() - startTime;
941
+ this.addBreadcrumb({
942
+ category: "fetch",
943
+ message: `${method} ${url} (failed)`,
944
+ data: {
945
+ url,
946
+ method,
947
+ error: error instanceof Error ? error.message : String(error),
948
+ duration: `${duration}ms`
949
+ },
950
+ level: "error"
951
+ });
952
+ throw error;
953
+ }
954
+ };
955
+ }
956
+ restoreFetch() {
957
+ if (this.originalFetch && typeof window !== "undefined") {
958
+ window.fetch = this.originalFetch;
959
+ this.originalFetch = null;
960
+ }
961
+ }
962
+ setupXHRWrapping() {
963
+ if (typeof XMLHttpRequest === "undefined") return;
964
+ const originalOpen = XMLHttpRequest.prototype.open;
965
+ const originalSend = XMLHttpRequest.prototype.send;
966
+ XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
967
+ this.__method = method;
968
+ this.__url = url.toString();
969
+ this.__startTime = Date.now();
970
+ return originalOpen.apply(this, arguments);
971
+ };
972
+ XMLHttpRequest.prototype.send = function(body) {
973
+ const xhr = this;
974
+ const method = xhr.__method || "GET";
975
+ const url = xhr.__url || "";
976
+ const originalOnReadyStateChange = xhr.onreadystatechange;
977
+ xhr.onreadystatechange = function() {
978
+ if (xhr.readyState === 4) {
979
+ const startTime = xhr.__startTime || Date.now();
980
+ const duration = Date.now() - startTime;
981
+ const breadcrumbManager = globalThis.__experienceBreadcrumbs;
982
+ if (breadcrumbManager) {
983
+ breadcrumbManager.addBreadcrumb({
984
+ category: "xhr",
985
+ message: `${method} ${url}`,
986
+ data: {
987
+ url,
988
+ method,
989
+ status: xhr.status,
990
+ duration: `${duration}ms`
991
+ },
992
+ level: xhr.status >= 200 && xhr.status < 300 ? "info" : "error"
993
+ });
994
+ }
995
+ }
996
+ if (originalOnReadyStateChange) {
997
+ originalOnReadyStateChange.apply(xhr, arguments);
998
+ }
999
+ };
1000
+ return originalSend.apply(this, arguments);
1001
+ };
1002
+ globalThis.__experienceBreadcrumbs = this;
1003
+ }
1004
+ setupNavigationListeners() {
1005
+ if (typeof window === "undefined" || typeof history === "undefined") return;
1006
+ const handlePopState = () => {
1007
+ this.addBreadcrumb({
1008
+ category: "navigation",
1009
+ message: "Navigation (popstate)",
1010
+ data: {
1011
+ url: window.location.href,
1012
+ type: "popstate"
1013
+ }
1014
+ });
1015
+ };
1016
+ window.addEventListener("popstate", handlePopState);
1017
+ this.unsubscribeFns.push(() => window.removeEventListener("popstate", handlePopState));
1018
+ const originalPushState = history.pushState;
1019
+ const originalReplaceState = history.replaceState;
1020
+ history.pushState = function(...args) {
1021
+ const breadcrumbManager = globalThis.__experienceBreadcrumbs;
1022
+ if (breadcrumbManager) {
1023
+ breadcrumbManager.addBreadcrumb({
1024
+ category: "navigation",
1025
+ message: "Navigation (pushState)",
1026
+ data: {
1027
+ url: args[2] || window.location.href,
1028
+ type: "pushState"
1029
+ }
1030
+ });
1031
+ }
1032
+ return originalPushState.apply(this, args);
1033
+ };
1034
+ history.replaceState = function(...args) {
1035
+ const breadcrumbManager = globalThis.__experienceBreadcrumbs;
1036
+ if (breadcrumbManager) {
1037
+ breadcrumbManager.addBreadcrumb({
1038
+ category: "navigation",
1039
+ message: "Navigation (replaceState)",
1040
+ data: {
1041
+ url: args[2] || window.location.href,
1042
+ type: "replaceState"
1043
+ }
1044
+ });
1045
+ }
1046
+ return originalReplaceState.apply(this, args);
1047
+ };
1048
+ this.unsubscribeFns.push(() => {
1049
+ history.pushState = originalPushState;
1050
+ history.replaceState = originalReplaceState;
1051
+ });
1052
+ }
1053
+ setupClickListener() {
1054
+ if (typeof document === "undefined") return;
1055
+ const handleClick = (event) => {
1056
+ const target = event.target;
1057
+ if (!target) return;
1058
+ const tagName = target.tagName?.toLowerCase();
1059
+ const selector = this.getElementSelector(target);
1060
+ this.addBreadcrumb({
1061
+ category: "click",
1062
+ message: `Click on ${tagName}`,
1063
+ data: {
1064
+ selector,
1065
+ tag: tagName,
1066
+ text: target.textContent?.slice(0, 100) || "",
1067
+ x: event.clientX,
1068
+ y: event.clientY
1069
+ }
1070
+ });
1071
+ };
1072
+ document.addEventListener("click", handleClick, true);
1073
+ this.unsubscribeFns.push(() => document.removeEventListener("click", handleClick, true));
1074
+ }
1075
+ getElementSelector(element) {
1076
+ try {
1077
+ const tag = element.tagName.toLowerCase();
1078
+ const id = element.id ? `#${element.id}` : "";
1079
+ const className = element.className && typeof element.className === "string" ? `.${element.className.split(" ").filter((c) => c.trim()).join(".")}` : "";
1080
+ return `${tag}${id}${className}`.slice(0, 100);
1081
+ } catch {
1082
+ return element.tagName?.toLowerCase() || "unknown";
1083
+ }
1084
+ }
1085
+ getBreadcrumbs() {
1086
+ return [...this.breadcrumbs];
1087
+ }
1088
+ clear() {
1089
+ this.breadcrumbs = [];
1090
+ }
1091
+ addCustomBreadcrumb(category, message, data) {
1092
+ this.addBreadcrumb({
1093
+ category,
1094
+ message,
1095
+ data
1096
+ });
1097
+ }
1098
+ };
1099
+
1100
+ // src/heatmap.ts
1101
+ var MOUSEMOVE_THROTTLE_MS = 100;
1102
+ var HOVER_THRESHOLD_MS = 500;
1103
+ var HeatmapTracker = class {
1104
+ constructor(batcher, sessionId) {
1105
+ this.isEnabled = false;
1106
+ this.unsubscribeFns = [];
1107
+ this.lastMouseMoveTime = 0;
1108
+ this.hoverTimer = null;
1109
+ this.lastHoverPosition = null;
1110
+ this.batcher = batcher;
1111
+ this.sessionId = sessionId;
1112
+ }
1113
+ enable() {
1114
+ if (this.isEnabled || typeof document === "undefined") return;
1115
+ this.isEnabled = true;
1116
+ this.setupMouseMoveTracking();
1117
+ this.setupClickTracking();
1118
+ this.setupHoverTracking();
1119
+ }
1120
+ disable() {
1121
+ if (!this.isEnabled) return;
1122
+ this.isEnabled = false;
1123
+ if (this.hoverTimer !== null) {
1124
+ clearTimeout(this.hoverTimer);
1125
+ this.hoverTimer = null;
1126
+ }
1127
+ this.unsubscribeFns.forEach((fn) => fn());
1128
+ this.unsubscribeFns = [];
1129
+ }
1130
+ setupMouseMoveTracking() {
1131
+ const handleMouseMove = (event) => {
1132
+ const now = Date.now();
1133
+ if (now - this.lastMouseMoveTime < MOUSEMOVE_THROTTLE_MS) {
1134
+ return;
1135
+ }
1136
+ this.lastMouseMoveTime = now;
1137
+ const viewport = this.getViewportSize();
1138
+ const heatmapEvent = {
1139
+ sessionId: this.sessionId,
1140
+ timestamp: now,
1141
+ type: "move",
1142
+ x: event.clientX,
1143
+ y: event.clientY,
1144
+ viewportWidth: viewport.width,
1145
+ viewportHeight: viewport.height,
1146
+ url: window.location.href
1147
+ };
1148
+ this.batcher.addHeatmapEvent(heatmapEvent);
1149
+ };
1150
+ document.addEventListener("mousemove", handleMouseMove, { passive: true });
1151
+ this.unsubscribeFns.push(
1152
+ () => document.removeEventListener("mousemove", handleMouseMove)
1153
+ );
1154
+ }
1155
+ setupClickTracking() {
1156
+ const handleClick = (event) => {
1157
+ const viewport = this.getViewportSize();
1158
+ const heatmapEvent = {
1159
+ sessionId: this.sessionId,
1160
+ timestamp: Date.now(),
1161
+ type: "click",
1162
+ x: event.clientX,
1163
+ y: event.clientY,
1164
+ viewportWidth: viewport.width,
1165
+ viewportHeight: viewport.height,
1166
+ url: window.location.href
1167
+ };
1168
+ this.batcher.addHeatmapEvent(heatmapEvent);
1169
+ };
1170
+ document.addEventListener("click", handleClick, { passive: true });
1171
+ this.unsubscribeFns.push(
1172
+ () => document.removeEventListener("click", handleClick)
1173
+ );
1174
+ }
1175
+ setupHoverTracking() {
1176
+ const handleMouseEnter = (event) => {
1177
+ const x = event.clientX;
1178
+ const y = event.clientY;
1179
+ this.lastHoverPosition = { x, y };
1180
+ this.hoverTimer = window.setTimeout(() => {
1181
+ if (this.lastHoverPosition && Math.abs(this.lastHoverPosition.x - x) < 50 && Math.abs(this.lastHoverPosition.y - y) < 50) {
1182
+ const viewport = this.getViewportSize();
1183
+ const heatmapEvent = {
1184
+ sessionId: this.sessionId,
1185
+ timestamp: Date.now(),
1186
+ type: "hover",
1187
+ x: this.lastHoverPosition.x,
1188
+ y: this.lastHoverPosition.y,
1189
+ viewportWidth: viewport.width,
1190
+ viewportHeight: viewport.height,
1191
+ url: window.location.href
1192
+ };
1193
+ this.batcher.addHeatmapEvent(heatmapEvent);
1194
+ }
1195
+ }, HOVER_THRESHOLD_MS);
1196
+ };
1197
+ const handleMouseMove = (event) => {
1198
+ this.lastHoverPosition = { x: event.clientX, y: event.clientY };
1199
+ };
1200
+ const handleMouseLeave = () => {
1201
+ if (this.hoverTimer !== null) {
1202
+ clearTimeout(this.hoverTimer);
1203
+ this.hoverTimer = null;
1204
+ }
1205
+ this.lastHoverPosition = null;
1206
+ };
1207
+ let lastHoverMoveTime = 0;
1208
+ const throttledMouseMove = (event) => {
1209
+ const now = Date.now();
1210
+ if (now - lastHoverMoveTime >= 100) {
1211
+ lastHoverMoveTime = now;
1212
+ handleMouseMove(event);
1213
+ }
1214
+ };
1215
+ document.addEventListener("mouseenter", handleMouseEnter, { passive: true });
1216
+ document.addEventListener("mousemove", throttledMouseMove, { passive: true });
1217
+ document.addEventListener("mouseleave", handleMouseLeave, { passive: true });
1218
+ this.unsubscribeFns.push(() => {
1219
+ document.removeEventListener("mouseenter", handleMouseEnter);
1220
+ document.removeEventListener("mousemove", throttledMouseMove);
1221
+ document.removeEventListener("mouseleave", handleMouseLeave);
1222
+ });
1223
+ }
1224
+ getViewportSize() {
1225
+ if (typeof window === "undefined") {
1226
+ return { width: 1920, height: 1080 };
1227
+ }
1228
+ return {
1229
+ width: window.innerWidth || document.documentElement.clientWidth || 1920,
1230
+ height: window.innerHeight || document.documentElement.clientHeight || 1080
1231
+ };
1232
+ }
1233
+ updateSessionId(sessionId) {
1234
+ this.sessionId = sessionId;
1235
+ }
1236
+ // Manual event tracking for programmatic use
1237
+ trackClick(x, y) {
1238
+ if (!this.isEnabled) return;
1239
+ const viewport = this.getViewportSize();
1240
+ const heatmapEvent = {
1241
+ sessionId: this.sessionId,
1242
+ timestamp: Date.now(),
1243
+ type: "click",
1244
+ x: Math.round(x),
1245
+ y: Math.round(y),
1246
+ viewportWidth: viewport.width,
1247
+ viewportHeight: viewport.height,
1248
+ url: window.location.href
1249
+ };
1250
+ this.batcher.addHeatmapEvent(heatmapEvent);
1251
+ }
1252
+ trackHover(x, y) {
1253
+ if (!this.isEnabled) return;
1254
+ const viewport = this.getViewportSize();
1255
+ const heatmapEvent = {
1256
+ sessionId: this.sessionId,
1257
+ timestamp: Date.now(),
1258
+ type: "hover",
1259
+ x: Math.round(x),
1260
+ y: Math.round(y),
1261
+ viewportWidth: viewport.width,
1262
+ viewportHeight: viewport.height,
1263
+ url: window.location.href
1264
+ };
1265
+ this.batcher.addHeatmapEvent(heatmapEvent);
1266
+ }
1267
+ };
1268
+
1269
+ // src/errors.ts
1270
+ var ErrorCapture = class {
1271
+ constructor(batcher, breadcrumbManager, sessionId) {
1272
+ this.isEnabled = false;
1273
+ this.unsubscribeFns = [];
1274
+ this.originalWindowError = null;
1275
+ this.originalUnhandledRejection = null;
1276
+ this.batcher = batcher;
1277
+ this.breadcrumbManager = breadcrumbManager;
1278
+ this.sessionId = sessionId;
1279
+ }
1280
+ enable() {
1281
+ if (this.isEnabled || typeof window === "undefined") return;
1282
+ this.isEnabled = true;
1283
+ this.setupGlobalErrorHandler();
1284
+ this.setupUnhandledRejectionHandler();
1285
+ }
1286
+ disable() {
1287
+ if (!this.isEnabled) return;
1288
+ this.isEnabled = false;
1289
+ this.restoreOriginalHandlers();
1290
+ this.unsubscribeFns.forEach((fn) => fn());
1291
+ this.unsubscribeFns = [];
1292
+ }
1293
+ setupGlobalErrorHandler() {
1294
+ this.originalWindowError = window.onerror;
1295
+ window.onerror = (message, filename, lineno, colno, error) => {
1296
+ this.captureError({
1297
+ message: typeof message === "string" ? message : "Unknown error",
1298
+ filename,
1299
+ lineno,
1300
+ colno,
1301
+ stack: error?.stack,
1302
+ severity: 2 /* ERROR */
1303
+ });
1304
+ if (this.originalWindowError) {
1305
+ return this.originalWindowError.call(window, message, filename, lineno, colno, error);
1306
+ }
1307
+ return false;
1308
+ };
1309
+ }
1310
+ setupUnhandledRejectionHandler() {
1311
+ const handleUnhandledRejection = (event) => {
1312
+ let message = "Unhandled Promise Rejection";
1313
+ let stack;
1314
+ if (event.reason) {
1315
+ if (event.reason instanceof Error) {
1316
+ message = event.reason.message || "Unhandled Promise Rejection";
1317
+ stack = event.reason.stack;
1318
+ } else if (typeof event.reason === "string") {
1319
+ message = event.reason;
1320
+ } else {
1321
+ try {
1322
+ message = JSON.stringify(event.reason);
1323
+ } catch {
1324
+ message = "Unhandled Promise Rejection (non-serializable reason)";
1325
+ }
1326
+ }
1327
+ }
1328
+ this.captureError({
1329
+ message,
1330
+ stack,
1331
+ severity: 2 /* ERROR */
1332
+ });
1333
+ if (this.originalUnhandledRejection) {
1334
+ this.originalUnhandledRejection.call(window, event);
1335
+ }
1336
+ };
1337
+ this.originalUnhandledRejection = window.onunhandledrejection;
1338
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
1339
+ this.unsubscribeFns.push(() => {
1340
+ window.removeEventListener("unhandledrejection", handleUnhandledRejection);
1341
+ });
1342
+ }
1343
+ restoreOriginalHandlers() {
1344
+ if (typeof window !== "undefined") {
1345
+ if (this.originalWindowError !== null) {
1346
+ window.onerror = this.originalWindowError;
1347
+ this.originalWindowError = null;
1348
+ }
1349
+ if (this.originalUnhandledRejection !== null) {
1350
+ window.onunhandledrejection = this.originalUnhandledRejection;
1351
+ this.originalUnhandledRejection = null;
1352
+ }
1353
+ }
1354
+ }
1355
+ getBrowserContext() {
1356
+ return {
1357
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "Unknown",
1358
+ url: typeof window !== "undefined" ? window.location.href : "Unknown",
1359
+ timestamp: Date.now(),
1360
+ viewport: {
1361
+ width: typeof window !== "undefined" ? window.innerWidth || 1920 : 1920,
1362
+ height: typeof window !== "undefined" ? window.innerHeight || 1080 : 1080
1363
+ }
1364
+ };
1365
+ }
1366
+ captureError(errorData) {
1367
+ const errorEvent = {
1368
+ sessionId: this.sessionId,
1369
+ timestamp: Date.now(),
1370
+ message: errorData.message,
1371
+ filename: errorData.filename,
1372
+ lineno: errorData.lineno,
1373
+ colno: errorData.colno,
1374
+ stack: errorData.stack,
1375
+ severity: errorData.severity,
1376
+ breadcrumbs: this.breadcrumbManager.getBreadcrumbs(),
1377
+ browserContext: this.getBrowserContext(),
1378
+ tags: errorData.tags,
1379
+ extra: errorData.extra
1380
+ };
1381
+ this.batcher.addErrorEvent(errorEvent);
1382
+ this.breadcrumbManager.addCustomBreadcrumb("ui", `Error: ${errorData.message}`, {
1383
+ severity: Severity[errorData.severity].toLowerCase(),
1384
+ filename: errorData.filename,
1385
+ line: errorData.lineno
1386
+ });
1387
+ }
1388
+ // Public methods for manual error reporting
1389
+ captureException(error, options) {
1390
+ if (!this.isEnabled) return;
1391
+ const severity = options?.severity ?? 2 /* ERROR */;
1392
+ this.captureError({
1393
+ message: error.message || "Unknown error",
1394
+ stack: error.stack,
1395
+ severity,
1396
+ tags: options?.tags,
1397
+ extra: options?.extra
1398
+ });
1399
+ }
1400
+ captureMessage(message, severity = 0 /* INFO */, options) {
1401
+ if (!this.isEnabled) return;
1402
+ this.captureError({
1403
+ message,
1404
+ severity,
1405
+ tags: options?.tags,
1406
+ extra: options?.extra
1407
+ });
1408
+ }
1409
+ captureBreadcrumb(message, category = "ui", data) {
1410
+ this.breadcrumbManager.addCustomBreadcrumb(category, message, data);
1411
+ }
1412
+ updateSessionId(sessionId) {
1413
+ this.sessionId = sessionId;
1414
+ }
1415
+ // Utility method to extract error info from various error types
1416
+ extractErrorInfo(error) {
1417
+ if (error instanceof Error) {
1418
+ return {
1419
+ message: error.message || "Unknown error",
1420
+ stack: error.stack
1421
+ };
1422
+ }
1423
+ if (typeof error === "string") {
1424
+ return { message: error };
1425
+ }
1426
+ if (error && typeof error === "object") {
1427
+ try {
1428
+ return { message: JSON.stringify(error) };
1429
+ } catch {
1430
+ return { message: "Non-serializable error object" };
1431
+ }
1432
+ }
1433
+ return { message: "Unknown error type" };
1434
+ }
1435
+ // Helper method for severity string to enum conversion
1436
+ static getSeverityFromString(severity) {
1437
+ switch (severity.toLowerCase()) {
1438
+ case "info":
1439
+ return 0 /* INFO */;
1440
+ case "warning":
1441
+ case "warn":
1442
+ return 1 /* WARNING */;
1443
+ case "error":
1444
+ return 2 /* ERROR */;
1445
+ case "fatal":
1446
+ return 3 /* FATAL */;
1447
+ default:
1448
+ return 0 /* INFO */;
1449
+ }
1450
+ }
1451
+ // Test method for manual error triggering (useful for debugging)
1452
+ triggerTestError() {
1453
+ if (!this.isEnabled) return;
1454
+ try {
1455
+ throw new Error("Test error from ExperienceSDK");
1456
+ } catch (error) {
1457
+ this.captureException(error, {
1458
+ tags: { test: "true" },
1459
+ severity: 1 /* WARNING */
1460
+ });
1461
+ }
1462
+ }
1463
+ };
1464
+
1465
+ // src/auto-track.ts
1466
+ var AutoTracker = class {
1467
+ constructor(batcher, sessionId) {
1468
+ this.isEnabled = false;
1469
+ this.unsubscribeFns = [];
1470
+ this.lastUrl = null;
1471
+ this.pageLoadTime = Date.now();
1472
+ this.batcher = batcher;
1473
+ this.sessionId = sessionId;
1474
+ }
1475
+ enable() {
1476
+ if (this.isEnabled) return;
1477
+ this.isEnabled = true;
1478
+ this.trackInitialPageView();
1479
+ this.setupSPATracking();
1480
+ }
1481
+ disable() {
1482
+ if (!this.isEnabled) return;
1483
+ this.isEnabled = false;
1484
+ this.unsubscribeFns.forEach((fn) => fn());
1485
+ this.unsubscribeFns = [];
1486
+ }
1487
+ trackInitialPageView() {
1488
+ if (typeof window === "undefined") return;
1489
+ const loadTime = this.calculateLoadTime();
1490
+ this.trackPageView(loadTime);
1491
+ this.lastUrl = window.location.href;
1492
+ }
1493
+ calculateLoadTime() {
1494
+ if (typeof performance === "undefined" || !performance.timing) {
1495
+ return void 0;
1496
+ }
1497
+ const timing = performance.timing;
1498
+ if (timing.loadEventEnd && timing.navigationStart) {
1499
+ return timing.loadEventEnd - timing.navigationStart;
1500
+ }
1501
+ if (timing.domContentLoadedEventEnd && timing.navigationStart) {
1502
+ return timing.domContentLoadedEventEnd - timing.navigationStart;
1503
+ }
1504
+ return void 0;
1505
+ }
1506
+ setupSPATracking() {
1507
+ if (typeof window === "undefined" || typeof history === "undefined") return;
1508
+ const handlePopState = () => {
1509
+ setTimeout(() => {
1510
+ if (window.location.href !== this.lastUrl) {
1511
+ this.trackPageView();
1512
+ this.lastUrl = window.location.href;
1513
+ }
1514
+ }, 0);
1515
+ };
1516
+ window.addEventListener("popstate", handlePopState);
1517
+ this.unsubscribeFns.push(() => window.removeEventListener("popstate", handlePopState));
1518
+ const originalPushState = history.pushState;
1519
+ const originalReplaceState = history.replaceState;
1520
+ history.pushState = (...args) => {
1521
+ const result = originalPushState.apply(history, args);
1522
+ setTimeout(() => {
1523
+ if (window.location.href !== this.lastUrl) {
1524
+ this.trackPageView();
1525
+ this.lastUrl = window.location.href;
1526
+ }
1527
+ }, 0);
1528
+ return result;
1529
+ };
1530
+ history.replaceState = (...args) => {
1531
+ const result = originalReplaceState.apply(history, args);
1532
+ setTimeout(() => {
1533
+ if (window.location.href !== this.lastUrl) {
1534
+ this.trackPageView();
1535
+ this.lastUrl = window.location.href;
1536
+ }
1537
+ }, 0);
1538
+ return result;
1539
+ };
1540
+ this.unsubscribeFns.push(() => {
1541
+ history.pushState = originalPushState;
1542
+ history.replaceState = originalReplaceState;
1543
+ });
1544
+ }
1545
+ getPageContext(loadTime) {
1546
+ if (typeof window === "undefined" || typeof document === "undefined") {
1547
+ return {
1548
+ url: "unknown",
1549
+ title: "unknown",
1550
+ referrer: "",
1551
+ viewport: { width: 1920, height: 1080 }
1552
+ };
1553
+ }
1554
+ return {
1555
+ url: window.location.href,
1556
+ title: document.title || "",
1557
+ referrer: document.referrer || "",
1558
+ viewport: {
1559
+ width: window.innerWidth || document.documentElement.clientWidth || 1920,
1560
+ height: window.innerHeight || document.documentElement.clientHeight || 1080
1561
+ },
1562
+ loadTime
1563
+ };
1564
+ }
1565
+ trackPageView(loadTime) {
1566
+ const pageContext = this.getPageContext(loadTime);
1567
+ const trackEvent = {
1568
+ eventType: "page_view",
1569
+ sessionId: this.sessionId,
1570
+ timestamp: Date.now(),
1571
+ pageContext
1572
+ };
1573
+ this.batcher.addTrackEvent(trackEvent);
1574
+ }
1575
+ // Public method for manual page view tracking
1576
+ trackCurrentPage() {
1577
+ if (!this.isEnabled) return;
1578
+ this.trackPageView();
1579
+ if (typeof window !== "undefined") {
1580
+ this.lastUrl = window.location.href;
1581
+ }
1582
+ }
1583
+ // Track custom navigation events
1584
+ trackNavigation(eventType, metadata) {
1585
+ if (!this.isEnabled) return;
1586
+ const pageContext = this.getPageContext();
1587
+ const trackEvent = {
1588
+ eventType,
1589
+ sessionId: this.sessionId,
1590
+ timestamp: Date.now(),
1591
+ pageContext,
1592
+ metadata
1593
+ };
1594
+ this.batcher.addTrackEvent(trackEvent);
1595
+ }
1596
+ updateSessionId(sessionId) {
1597
+ this.sessionId = sessionId;
1598
+ }
1599
+ // Get current page info for debugging
1600
+ getCurrentPageInfo() {
1601
+ return this.getPageContext();
1602
+ }
1603
+ };
1604
+
1605
+ // src/data-t.ts
1606
+ var HOVER_THRESHOLD_MS2 = 500;
1607
+ var DataTTracker = class {
1608
+ constructor(batcher, sessionId) {
1609
+ this.isEnabled = false;
1610
+ this.unsubscribeFns = [];
1611
+ this.mutationObserver = null;
1612
+ this.intersectionObserver = null;
1613
+ this.visibleElements = /* @__PURE__ */ new Set();
1614
+ this.hoverTimers = /* @__PURE__ */ new Map();
1615
+ this.batcher = batcher;
1616
+ this.sessionId = sessionId;
40
1617
  }
41
- add(breadcrumb) {
42
- this.buffer.push({ ...breadcrumb, timestampMs: Date.now() });
43
- if (this.buffer.length > MAX_BREADCRUMBS) {
44
- this.buffer.shift();
1618
+ enable() {
1619
+ if (this.isEnabled || typeof document === "undefined") return;
1620
+ this.isEnabled = true;
1621
+ this.setupMutationObserver();
1622
+ this.setupEventDelegation();
1623
+ this.setupVisibilityTracking();
1624
+ this.setupHoverTracking();
1625
+ this.scanExistingElements();
1626
+ }
1627
+ disable() {
1628
+ if (!this.isEnabled) return;
1629
+ this.isEnabled = false;
1630
+ if (this.mutationObserver) {
1631
+ this.mutationObserver.disconnect();
1632
+ this.mutationObserver = null;
1633
+ }
1634
+ if (this.intersectionObserver) {
1635
+ this.intersectionObserver.disconnect();
1636
+ this.intersectionObserver = null;
45
1637
  }
1638
+ this.hoverTimers.forEach((timer) => clearTimeout(timer));
1639
+ this.hoverTimers.clear();
1640
+ this.unsubscribeFns.forEach((fn) => fn());
1641
+ this.unsubscribeFns = [];
1642
+ this.visibleElements.clear();
1643
+ }
1644
+ setupMutationObserver() {
1645
+ if (typeof MutationObserver === "undefined") return;
1646
+ this.mutationObserver = new MutationObserver((mutations) => {
1647
+ for (const mutation of mutations) {
1648
+ if (mutation.type === "childList") {
1649
+ mutation.addedNodes.forEach((node) => {
1650
+ if (node.nodeType === Node.ELEMENT_NODE) {
1651
+ const element = node;
1652
+ this.processNewElement(element);
1653
+ const descendants = element.querySelectorAll("[data-t]");
1654
+ descendants.forEach((desc) => this.processNewElement(desc));
1655
+ }
1656
+ });
1657
+ } else if (mutation.type === "attributes" && mutation.attributeName === "data-t") {
1658
+ const target = mutation.target;
1659
+ this.processNewElement(target);
1660
+ }
1661
+ }
1662
+ });
1663
+ this.mutationObserver.observe(document, {
1664
+ childList: true,
1665
+ subtree: true,
1666
+ attributes: true,
1667
+ attributeFilter: ["data-t"]
1668
+ });
46
1669
  }
47
- getAll() {
48
- return [...this.buffer];
1670
+ setupEventDelegation() {
1671
+ const handleClick = (event) => {
1672
+ const target = event.target;
1673
+ if (!target) return;
1674
+ const dataTElement = this.findDataTParent(target);
1675
+ if (dataTElement) {
1676
+ this.trackDataTEvent("click", dataTElement, {
1677
+ x: event.clientX,
1678
+ y: event.clientY
1679
+ });
1680
+ }
1681
+ };
1682
+ document.addEventListener("click", handleClick, true);
1683
+ this.unsubscribeFns.push(
1684
+ () => document.removeEventListener("click", handleClick, true)
1685
+ );
49
1686
  }
50
- install() {
51
- ["log", "warn", "error"].forEach((level) => {
52
- const original = this.originalConsole[level];
53
- console[level] = (...args) => {
54
- this.add({
55
- type: "console",
56
- category: level,
57
- message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")
1687
+ setupVisibilityTracking() {
1688
+ if (typeof IntersectionObserver === "undefined") return;
1689
+ this.intersectionObserver = new IntersectionObserver(
1690
+ (entries) => {
1691
+ entries.forEach((entry) => {
1692
+ const element = entry.target;
1693
+ const dataTValue = element.getAttribute("data-t");
1694
+ if (!dataTValue) return;
1695
+ if (entry.isIntersecting && !this.visibleElements.has(element)) {
1696
+ this.visibleElements.add(element);
1697
+ this.trackDataTEvent("view", element);
1698
+ } else if (!entry.isIntersecting && this.visibleElements.has(element)) {
1699
+ this.visibleElements.delete(element);
1700
+ this.trackDataTEvent("hide", element);
1701
+ }
58
1702
  });
59
- original.apply(console, args);
60
- };
1703
+ },
1704
+ {
1705
+ threshold: 0.1,
1706
+ // Trigger when at least 10% visible
1707
+ rootMargin: "50px"
1708
+ // Start observing 50px before entering viewport
1709
+ }
1710
+ );
1711
+ }
1712
+ setupHoverTracking() {
1713
+ const handleMouseEnter = (event) => {
1714
+ const target = event.target;
1715
+ const dataTElement = this.findDataTParent(target);
1716
+ if (!dataTElement) return;
1717
+ const timer = window.setTimeout(() => {
1718
+ this.trackDataTEvent("hover", dataTElement);
1719
+ this.hoverTimers.delete(dataTElement);
1720
+ }, HOVER_THRESHOLD_MS2);
1721
+ this.hoverTimers.set(dataTElement, timer);
1722
+ };
1723
+ const handleMouseLeave = (event) => {
1724
+ const target = event.target;
1725
+ const dataTElement = this.findDataTParent(target);
1726
+ if (!dataTElement) return;
1727
+ const timer = this.hoverTimers.get(dataTElement);
1728
+ if (timer) {
1729
+ clearTimeout(timer);
1730
+ this.hoverTimers.delete(dataTElement);
1731
+ }
1732
+ };
1733
+ document.addEventListener("mouseenter", handleMouseEnter, true);
1734
+ document.addEventListener("mouseleave", handleMouseLeave, true);
1735
+ this.unsubscribeFns.push(() => {
1736
+ document.removeEventListener("mouseenter", handleMouseEnter, true);
1737
+ document.removeEventListener("mouseleave", handleMouseLeave, true);
61
1738
  });
62
- if (typeof fetch !== "undefined") {
63
- this.originalFetch = fetch;
64
- const self = this;
65
- window.fetch = function(input, init) {
66
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
67
- const method = init?.method || "GET";
68
- self.add({ type: "http", category: "fetch", message: `${method} ${url}` });
69
- return self.originalFetch.call(window, input, init);
70
- };
1739
+ }
1740
+ scanExistingElements() {
1741
+ const existingElements = document.querySelectorAll("[data-t]");
1742
+ existingElements.forEach((element) => this.processNewElement(element));
1743
+ }
1744
+ processNewElement(element) {
1745
+ const dataTValue = element.getAttribute("data-t");
1746
+ if (!dataTValue) return;
1747
+ if (this.intersectionObserver) {
1748
+ this.intersectionObserver.observe(element);
1749
+ }
1750
+ this.trackDataTEvent("discover", element);
1751
+ }
1752
+ findDataTParent(element) {
1753
+ let current = element;
1754
+ while (current) {
1755
+ if (current.hasAttribute("data-t")) {
1756
+ return current;
1757
+ }
1758
+ current = current.parentElement;
1759
+ }
1760
+ return null;
1761
+ }
1762
+ getElementSelector(element) {
1763
+ try {
1764
+ const tag = element.tagName.toLowerCase();
1765
+ const id = element.id ? `#${element.id}` : "";
1766
+ const classes = element.className && typeof element.className === "string" ? `.${element.className.split(" ").filter((c) => c.trim()).join(".")}` : "";
1767
+ let selector = `${tag}${id}${classes}`;
1768
+ const parent = element.parentElement;
1769
+ if (parent && !id) {
1770
+ const siblings = Array.from(parent.children).filter(
1771
+ (child) => child.tagName === element.tagName
1772
+ );
1773
+ if (siblings.length > 1) {
1774
+ const index = siblings.indexOf(element) + 1;
1775
+ selector += `:nth-child(${index})`;
1776
+ }
1777
+ }
1778
+ return selector.slice(0, 200);
1779
+ } catch (error) {
1780
+ return element.tagName?.toLowerCase() || "unknown";
71
1781
  }
72
- if (typeof XMLHttpRequest !== "undefined") {
73
- this.originalXhrOpen = XMLHttpRequest.prototype.open;
74
- const self = this;
75
- XMLHttpRequest.prototype.open = function(method, url) {
76
- self.add({ type: "http", category: "xhr", message: `${method} ${url}` });
77
- return self.originalXhrOpen.apply(this, arguments);
1782
+ }
1783
+ getElementContext(element, position) {
1784
+ return {
1785
+ selector: this.getElementSelector(element),
1786
+ dataT: element.getAttribute("data-t") || void 0,
1787
+ tag: element.tagName.toLowerCase(),
1788
+ position
1789
+ };
1790
+ }
1791
+ getPageContext() {
1792
+ if (typeof window === "undefined" || typeof document === "undefined") {
1793
+ return {
1794
+ url: "unknown",
1795
+ title: "unknown",
1796
+ referrer: "",
1797
+ viewport: { width: 1920, height: 1080 }
78
1798
  };
79
1799
  }
80
- this.clickHandler = (e) => {
81
- const target = e.target;
82
- if (!target) return;
83
- const tag = target.tagName?.toLowerCase() || "";
84
- const text = (target.textContent || "").slice(0, 100);
85
- this.add({ type: "ui", category: "click", message: `${tag}: ${text}` });
1800
+ return {
1801
+ url: window.location.href,
1802
+ title: document.title || "",
1803
+ referrer: document.referrer || "",
1804
+ viewport: {
1805
+ width: window.innerWidth || 1920,
1806
+ height: window.innerHeight || 1080
1807
+ }
86
1808
  };
87
- document.addEventListener("click", this.clickHandler, true);
88
1809
  }
89
- teardown() {
90
- console.log = this.originalConsole.log;
91
- console.warn = this.originalConsole.warn;
92
- console.error = this.originalConsole.error;
93
- if (this.originalFetch) {
94
- window.fetch = this.originalFetch;
1810
+ trackDataTEvent(eventType, element, position) {
1811
+ const dataTValue = element.getAttribute("data-t");
1812
+ if (!dataTValue) return;
1813
+ const elementContext = this.getElementContext(element, position);
1814
+ const pageContext = this.getPageContext();
1815
+ const trackEvent = {
1816
+ eventType: `data_t_${eventType}`,
1817
+ sessionId: this.sessionId,
1818
+ timestamp: Date.now(),
1819
+ pageContext,
1820
+ elementContext,
1821
+ metadata: {
1822
+ data_t: dataTValue,
1823
+ event_type: eventType
1824
+ }
1825
+ };
1826
+ this.batcher.addTrackEvent(trackEvent);
1827
+ }
1828
+ updateSessionId(sessionId) {
1829
+ this.sessionId = sessionId;
1830
+ }
1831
+ };
1832
+
1833
+ // src/flags.ts
1834
+ var FeatureFlagClient = class {
1835
+ constructor(transport, appId, debug = false) {
1836
+ this.profileId = null;
1837
+ this.flagCache = /* @__PURE__ */ new Map();
1838
+ this.cacheTimeoutMs = 5 * 60 * 1e3;
1839
+ // 5 minutes
1840
+ this.eventSource = null;
1841
+ this._backgroundFetchInProgress = false;
1842
+ this.transport = transport;
1843
+ this.appId = appId;
1844
+ this.debug = debug;
1845
+ }
1846
+ setProfileId(profileId) {
1847
+ this.profileId = profileId;
1848
+ this.flagCache.clear();
1849
+ }
1850
+ async fetchFlags() {
1851
+ try {
1852
+ const flags = await this.transport.fetchFlags(this.appId, this.profileId || void 0);
1853
+ const now = Date.now();
1854
+ for (const [key, value] of Object.entries(flags)) {
1855
+ this.flagCache.set(key, {
1856
+ value,
1857
+ fetchedAt: now
1858
+ });
1859
+ }
1860
+ if (this.debug) {
1861
+ console.log("[ExperienceSDK] Flags fetched:", flags);
1862
+ }
1863
+ } catch (error) {
1864
+ if (this.debug) {
1865
+ console.error("[ExperienceSDK] Failed to fetch flags:", error);
1866
+ }
1867
+ }
1868
+ }
1869
+ getFlag(key, defaultValue) {
1870
+ const cached = this.flagCache.get(key);
1871
+ if (cached && Date.now() - cached.fetchedAt < this.cacheTimeoutMs) {
1872
+ if (this.debug) {
1873
+ console.log(`[ExperienceSDK] Flag '${key}' from cache:`, cached.value);
1874
+ }
1875
+ return this.convertValue(cached.value, defaultValue, key);
1876
+ }
1877
+ this.fetchFlagsInBackground();
1878
+ if (this.debug) {
1879
+ console.log(`[ExperienceSDK] Flag '${key}' not in cache, returning default:`, defaultValue);
1880
+ }
1881
+ return defaultValue;
1882
+ }
1883
+ getFlagSync(key, defaultValue) {
1884
+ const cached = this.flagCache.get(key);
1885
+ if (cached) {
1886
+ return this.convertValue(cached.value, defaultValue, key);
1887
+ }
1888
+ return defaultValue;
1889
+ }
1890
+ convertValue(value, defaultValue, key) {
1891
+ try {
1892
+ if (typeof value === typeof defaultValue) {
1893
+ return value;
1894
+ }
1895
+ if (typeof defaultValue === "boolean") {
1896
+ if (typeof value === "string") {
1897
+ return value.toLowerCase() === "true" || value === "1";
1898
+ }
1899
+ return Boolean(value);
1900
+ }
1901
+ if (typeof defaultValue === "number") {
1902
+ const num = Number(value);
1903
+ return isNaN(num) ? defaultValue : num;
1904
+ }
1905
+ if (typeof defaultValue === "string") {
1906
+ return String(value);
1907
+ }
1908
+ if (typeof defaultValue === "object" && typeof value === "string") {
1909
+ try {
1910
+ return JSON.parse(value);
1911
+ } catch {
1912
+ return defaultValue;
1913
+ }
1914
+ }
1915
+ return value;
1916
+ } catch (error) {
1917
+ if (this.debug) {
1918
+ console.warn(`[ExperienceSDK] Failed to convert flag value for '${key}':`, error);
1919
+ }
1920
+ return defaultValue;
1921
+ }
1922
+ }
1923
+ fetchFlagsInBackground() {
1924
+ if (this._backgroundFetchInProgress) {
1925
+ return;
1926
+ }
1927
+ this._backgroundFetchInProgress = true;
1928
+ this.fetchFlags().finally(() => {
1929
+ this._backgroundFetchInProgress = false;
1930
+ });
1931
+ }
1932
+ // Initialize with immediate fetch
1933
+ async initialize() {
1934
+ await this.fetchFlags();
1935
+ }
1936
+ // Set up real-time flag updates via Server-Sent Events
1937
+ setupRealtimeUpdates(endpoint) {
1938
+ if (typeof EventSource === "undefined") {
1939
+ if (this.debug) {
1940
+ console.warn("[ExperienceSDK] EventSource not supported, real-time flag updates disabled");
1941
+ }
1942
+ return;
1943
+ }
1944
+ this.closeRealtimeUpdates();
1945
+ const baseEndpoint = endpoint || this.transport["config"].endpoint;
1946
+ const streamUrl = `${baseEndpoint}/api/v1/flags/${this.appId}/resolve/stream`;
1947
+ const url = new URL(streamUrl);
1948
+ if (this.profileId) {
1949
+ url.searchParams.set("profileId", this.profileId);
1950
+ }
1951
+ try {
1952
+ this.eventSource = new EventSource(url.toString());
1953
+ this.eventSource.onopen = () => {
1954
+ if (this.debug) {
1955
+ console.log("[ExperienceSDK] Real-time flag updates connected");
1956
+ }
1957
+ };
1958
+ this.eventSource.onmessage = (event) => {
1959
+ try {
1960
+ const data = JSON.parse(event.data);
1961
+ if (data.flags) {
1962
+ const now = Date.now();
1963
+ for (const [key, value] of Object.entries(data.flags)) {
1964
+ this.flagCache.set(key, {
1965
+ value,
1966
+ fetchedAt: now
1967
+ });
1968
+ }
1969
+ if (this.debug) {
1970
+ console.log("[ExperienceSDK] Flag updates received:", data.flags);
1971
+ }
1972
+ }
1973
+ } catch (error) {
1974
+ if (this.debug) {
1975
+ console.error("[ExperienceSDK] Failed to parse flag update:", error);
1976
+ }
1977
+ }
1978
+ };
1979
+ this.eventSource.onerror = (error) => {
1980
+ if (this.debug) {
1981
+ console.error("[ExperienceSDK] Real-time flag updates error:", error);
1982
+ }
1983
+ setTimeout(() => {
1984
+ if (this.eventSource?.readyState === EventSource.CLOSED) {
1985
+ this.setupRealtimeUpdates(endpoint);
1986
+ }
1987
+ }, 5e3);
1988
+ };
1989
+ } catch (error) {
1990
+ if (this.debug) {
1991
+ console.error("[ExperienceSDK] Failed to setup real-time flag updates:", error);
1992
+ }
1993
+ }
1994
+ }
1995
+ closeRealtimeUpdates() {
1996
+ if (this.eventSource) {
1997
+ this.eventSource.close();
1998
+ this.eventSource = null;
1999
+ if (this.debug) {
2000
+ console.log("[ExperienceSDK] Real-time flag updates disconnected");
2001
+ }
2002
+ }
2003
+ }
2004
+ // Clear all cached flags
2005
+ clearCache() {
2006
+ this.flagCache.clear();
2007
+ if (this.debug) {
2008
+ console.log("[ExperienceSDK] Flag cache cleared");
2009
+ }
2010
+ }
2011
+ // Get cache status for debugging
2012
+ getCacheInfo() {
2013
+ const keys = Array.from(this.flagCache.keys());
2014
+ const now = Date.now();
2015
+ let oldestEntry;
2016
+ let newestEntry;
2017
+ for (const [cacheKey, entry] of this.flagCache.entries()) {
2018
+ const age = now - entry.fetchedAt;
2019
+ if (!oldestEntry || age > oldestEntry.age) {
2020
+ oldestEntry = { key: cacheKey, age };
2021
+ }
2022
+ if (!newestEntry || age < newestEntry.age) {
2023
+ newestEntry = { key: cacheKey, age };
2024
+ }
95
2025
  }
96
- if (this.originalXhrOpen) {
97
- XMLHttpRequest.prototype.open = this.originalXhrOpen;
2026
+ return {
2027
+ size: this.flagCache.size,
2028
+ keys,
2029
+ oldestEntry,
2030
+ newestEntry
2031
+ };
2032
+ }
2033
+ // Check if a flag exists in cache
2034
+ hasCachedFlag(key) {
2035
+ return this.flagCache.has(key);
2036
+ }
2037
+ // Get all cached flags (for debugging)
2038
+ getAllCachedFlags() {
2039
+ const result = {};
2040
+ for (const [key, entry] of this.flagCache.entries()) {
2041
+ result[key] = entry.value;
98
2042
  }
99
- if (this.clickHandler) {
100
- document.removeEventListener("click", this.clickHandler, true);
2043
+ return result;
2044
+ }
2045
+ // Preload specific flags
2046
+ async preloadFlags(keys) {
2047
+ const missing = keys.filter((key) => !this.hasCachedFlag(key));
2048
+ if (missing.length > 0) {
2049
+ await this.fetchFlags();
101
2050
  }
102
2051
  }
2052
+ shutdown() {
2053
+ this.closeRealtimeUpdates();
2054
+ this.flagCache.clear();
2055
+ }
103
2056
  };
104
2057
 
105
2058
  // src/offline.ts
106
- var DB_NAME = "shellapps_experience";
107
- var STORE_NAME = "offline_queue";
2059
+ var DB_NAME = "ExperienceSDKOffline";
108
2060
  var DB_VERSION = 1;
109
- function openDB() {
110
- return new Promise((resolve, reject) => {
111
- const request = indexedDB.open(DB_NAME, DB_VERSION);
112
- request.onupgradeneeded = () => {
113
- const db = request.result;
114
- if (!db.objectStoreNames.contains(STORE_NAME)) {
115
- db.createObjectStore(STORE_NAME, { autoIncrement: true });
116
- }
117
- };
118
- request.onsuccess = () => resolve(request.result);
119
- request.onerror = () => reject(request.error);
120
- });
121
- }
122
- async function enqueueOffline(data) {
123
- const db = await openDB();
124
- const tx = db.transaction(STORE_NAME, "readwrite");
125
- tx.objectStore(STORE_NAME).add(data);
126
- await new Promise((resolve, reject) => {
127
- tx.oncomplete = () => resolve();
128
- tx.onerror = () => reject(tx.error);
129
- });
130
- db.close();
131
- }
132
- async function drainOfflineQueue(sendFn) {
133
- const db = await openDB();
134
- const tx = db.transaction(STORE_NAME, "readwrite");
135
- const store = tx.objectStore(STORE_NAME);
136
- const cursorReq = store.openCursor();
137
- await new Promise((resolve, reject) => {
138
- cursorReq.onsuccess = async () => {
139
- const cursor = cursorReq.result;
140
- if (!cursor) {
141
- resolve();
2061
+ var EVENTS_STORE = "events";
2062
+ var ERRORS_STORE = "errors";
2063
+ var MAX_OFFLINE_ITEMS = 1e3;
2064
+ var OfflineQueue = class {
2065
+ constructor(transport, debug = false) {
2066
+ this.db = null;
2067
+ this.isInitialized = false;
2068
+ this.unsubscribeFns = [];
2069
+ this.isProcessing = false;
2070
+ this.transport = transport;
2071
+ this.debug = debug;
2072
+ this.initialize();
2073
+ this.setupOnlineListener();
2074
+ }
2075
+ async initialize() {
2076
+ if (typeof indexedDB === "undefined") {
2077
+ if (this.debug) {
2078
+ console.warn("[ExperienceSDK] IndexedDB not available, offline queue disabled");
2079
+ }
2080
+ return;
2081
+ }
2082
+ try {
2083
+ this.db = await this.openDatabase();
2084
+ this.isInitialized = true;
2085
+ if (this.debug) {
2086
+ console.log("[ExperienceSDK] Offline queue initialized");
2087
+ }
2088
+ this.processStoredItems();
2089
+ } catch (error) {
2090
+ if (this.debug) {
2091
+ console.error("[ExperienceSDK] Failed to initialize offline queue:", error);
2092
+ }
2093
+ }
2094
+ }
2095
+ openDatabase() {
2096
+ return new Promise((resolve, reject) => {
2097
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
2098
+ request.onerror = () => reject(request.error);
2099
+ request.onsuccess = () => resolve(request.result);
2100
+ request.onupgradeneeded = (event) => {
2101
+ const db = event.target.result;
2102
+ if (!db.objectStoreNames.contains(EVENTS_STORE)) {
2103
+ const eventsStore = db.createObjectStore(EVENTS_STORE, { keyPath: "id" });
2104
+ eventsStore.createIndex("timestamp", "timestamp");
2105
+ eventsStore.createIndex("type", "type");
2106
+ }
2107
+ if (!db.objectStoreNames.contains(ERRORS_STORE)) {
2108
+ const errorsStore = db.createObjectStore(ERRORS_STORE, { keyPath: "id" });
2109
+ errorsStore.createIndex("timestamp", "timestamp");
2110
+ errorsStore.createIndex("type", "type");
2111
+ }
2112
+ };
2113
+ });
2114
+ }
2115
+ setupOnlineListener() {
2116
+ if (typeof window === "undefined") return;
2117
+ const handleOnline = () => {
2118
+ if (this.debug) {
2119
+ console.log("[ExperienceSDK] Network online, processing offline queue");
2120
+ }
2121
+ this.processStoredItems();
2122
+ };
2123
+ window.addEventListener("online", handleOnline);
2124
+ this.unsubscribeFns.push(
2125
+ () => window.removeEventListener("online", handleOnline)
2126
+ );
2127
+ }
2128
+ generateId() {
2129
+ return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2130
+ }
2131
+ async storeEventBatch(batch) {
2132
+ if (!this.isInitialized || !this.db) return;
2133
+ const storedBatch = {
2134
+ id: this.generateId(),
2135
+ timestamp: Date.now(),
2136
+ type: "events",
2137
+ data: batch,
2138
+ retryCount: 0
2139
+ };
2140
+ try {
2141
+ await this.storeItem(EVENTS_STORE, storedBatch);
2142
+ if (this.debug) {
2143
+ console.log("[ExperienceSDK] Event batch stored offline:", storedBatch.id);
2144
+ }
2145
+ this.cleanupOldItems(EVENTS_STORE);
2146
+ } catch (error) {
2147
+ if (this.debug) {
2148
+ console.error("[ExperienceSDK] Failed to store event batch offline:", error);
2149
+ }
2150
+ }
2151
+ }
2152
+ async storeErrorBatch(batch) {
2153
+ if (!this.isInitialized || !this.db) return;
2154
+ const storedBatch = {
2155
+ id: this.generateId(),
2156
+ timestamp: Date.now(),
2157
+ type: "errors",
2158
+ data: batch,
2159
+ retryCount: 0
2160
+ };
2161
+ try {
2162
+ await this.storeItem(ERRORS_STORE, storedBatch);
2163
+ if (this.debug) {
2164
+ console.log("[ExperienceSDK] Error batch stored offline:", storedBatch.id);
2165
+ }
2166
+ this.cleanupOldItems(ERRORS_STORE);
2167
+ } catch (error) {
2168
+ if (this.debug) {
2169
+ console.error("[ExperienceSDK] Failed to store error batch offline:", error);
2170
+ }
2171
+ }
2172
+ }
2173
+ storeItem(storeName, item) {
2174
+ return new Promise((resolve, reject) => {
2175
+ if (!this.db) {
2176
+ reject(new Error("Database not initialized"));
2177
+ return;
2178
+ }
2179
+ const transaction = this.db.transaction([storeName], "readwrite");
2180
+ const store = transaction.objectStore(storeName);
2181
+ const request = store.put(item);
2182
+ request.onerror = () => reject(request.error);
2183
+ request.onsuccess = () => resolve();
2184
+ });
2185
+ }
2186
+ async cleanupOldItems(storeName) {
2187
+ if (!this.db) return;
2188
+ try {
2189
+ const items = await this.getAllItems(storeName);
2190
+ if (items.length > MAX_OFFLINE_ITEMS) {
2191
+ const itemsToRemove = items.sort((a, b) => a.timestamp - b.timestamp).slice(0, items.length - MAX_OFFLINE_ITEMS);
2192
+ await Promise.all(
2193
+ itemsToRemove.map((item) => this.removeItem(storeName, item.id))
2194
+ );
2195
+ if (this.debug) {
2196
+ console.log(`[ExperienceSDK] Cleaned up ${itemsToRemove.length} old offline items`);
2197
+ }
2198
+ }
2199
+ } catch (error) {
2200
+ if (this.debug) {
2201
+ console.error("[ExperienceSDK] Failed to cleanup old items:", error);
2202
+ }
2203
+ }
2204
+ }
2205
+ getAllItems(storeName) {
2206
+ return new Promise((resolve, reject) => {
2207
+ if (!this.db) {
2208
+ reject(new Error("Database not initialized"));
2209
+ return;
2210
+ }
2211
+ const transaction = this.db.transaction([storeName], "readonly");
2212
+ const store = transaction.objectStore(storeName);
2213
+ const request = store.getAll();
2214
+ request.onerror = () => reject(request.error);
2215
+ request.onsuccess = () => resolve(request.result);
2216
+ });
2217
+ }
2218
+ removeItem(storeName, id) {
2219
+ return new Promise((resolve, reject) => {
2220
+ if (!this.db) {
2221
+ reject(new Error("Database not initialized"));
142
2222
  return;
143
2223
  }
144
- const item = cursor.value;
2224
+ const transaction = this.db.transaction([storeName], "readwrite");
2225
+ const store = transaction.objectStore(storeName);
2226
+ const request = store.delete(id);
2227
+ request.onerror = () => reject(request.error);
2228
+ request.onsuccess = () => resolve();
2229
+ });
2230
+ }
2231
+ async processStoredItems() {
2232
+ if (!this.isInitialized || !this.db || this.isProcessing) return;
2233
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
2234
+ if (this.debug) {
2235
+ console.log("[ExperienceSDK] Still offline, skipping queue processing");
2236
+ }
2237
+ return;
2238
+ }
2239
+ this.isProcessing = true;
2240
+ try {
2241
+ await this.processStoreItems(EVENTS_STORE);
2242
+ await this.processStoreItems(ERRORS_STORE);
2243
+ } finally {
2244
+ this.isProcessing = false;
2245
+ }
2246
+ }
2247
+ async processStoreItems(storeName) {
2248
+ try {
2249
+ const items = await this.getAllItems(storeName);
2250
+ if (items.length === 0) return;
2251
+ if (this.debug) {
2252
+ console.log(`[ExperienceSDK] Processing ${items.length} offline ${storeName}`);
2253
+ }
2254
+ items.sort((a, b) => a.timestamp - b.timestamp);
2255
+ for (const item of items) {
2256
+ try {
2257
+ let success = false;
2258
+ if (storeName === EVENTS_STORE && item.type === "events") {
2259
+ const result = await this.transport.sendEventBatch(item.data);
2260
+ success = result.success !== false;
2261
+ } else if (storeName === ERRORS_STORE && item.type === "errors") {
2262
+ const result = await this.transport.sendErrorBatch(item.data);
2263
+ success = result.success !== false;
2264
+ }
2265
+ if (success) {
2266
+ await this.removeItem(storeName, item.id);
2267
+ if (this.debug) {
2268
+ console.log(`[ExperienceSDK] Offline ${item.type} sent successfully:`, item.id);
2269
+ }
2270
+ } else {
2271
+ throw new Error("Send failed");
2272
+ }
2273
+ } catch (error) {
2274
+ if (this.debug) {
2275
+ console.error(`[ExperienceSDK] Failed to send offline ${item.type}:`, error);
2276
+ }
2277
+ item.retryCount++;
2278
+ if (item.retryCount >= 3) {
2279
+ await this.removeItem(storeName, item.id);
2280
+ if (this.debug) {
2281
+ console.log(`[ExperienceSDK] Removing ${item.type} after ${item.retryCount} retries:`, item.id);
2282
+ }
2283
+ } else {
2284
+ await this.storeItem(storeName, item);
2285
+ }
2286
+ }
2287
+ }
2288
+ } catch (error) {
2289
+ if (this.debug) {
2290
+ console.error(`[ExperienceSDK] Failed to process offline ${storeName}:`, error);
2291
+ }
2292
+ }
2293
+ }
2294
+ // Get queue status for debugging
2295
+ async getQueueStatus() {
2296
+ let eventCount = 0;
2297
+ let errorCount = 0;
2298
+ if (this.isInitialized && this.db) {
145
2299
  try {
146
- await sendFn(item.url, item.body);
147
- cursor.delete();
148
- cursor.continue();
149
- } catch {
150
- resolve();
2300
+ const events = await this.getAllItems(EVENTS_STORE);
2301
+ const errors = await this.getAllItems(ERRORS_STORE);
2302
+ eventCount = events.length;
2303
+ errorCount = errors.length;
2304
+ } catch (error) {
151
2305
  }
2306
+ }
2307
+ return {
2308
+ events: eventCount,
2309
+ errors: errorCount,
2310
+ isOnline: typeof navigator !== "undefined" ? navigator.onLine : true,
2311
+ isInitialized: this.isInitialized
152
2312
  };
153
- cursorReq.onerror = () => reject(cursorReq.error);
154
- });
155
- db.close();
156
- }
157
-
158
- // src/utils.ts
159
- function generateUUID() {
160
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
161
- return crypto.randomUUID();
162
- }
163
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
164
- const r = Math.random() * 16 | 0;
165
- const v = c === "x" ? r : r & 3 | 8;
166
- return v.toString(16);
167
- });
168
- }
169
- function getPageContext() {
170
- return {
171
- path: location.pathname,
172
- title: document.title,
173
- referrer: document.referrer,
174
- viewportWidth: window.innerWidth,
175
- viewportHeight: window.innerHeight
176
- };
177
- }
178
- function throttle(fn, ms) {
179
- let last = 0;
180
- return ((...args) => {
181
- const now = Date.now();
182
- if (now - last >= ms) {
183
- last = now;
184
- fn(...args);
2313
+ }
2314
+ // Clear all offline data
2315
+ async clear() {
2316
+ if (!this.isInitialized || !this.db) return;
2317
+ try {
2318
+ const transaction = this.db.transaction([EVENTS_STORE, ERRORS_STORE], "readwrite");
2319
+ await Promise.all([
2320
+ new Promise((resolve, reject) => {
2321
+ const request = transaction.objectStore(EVENTS_STORE).clear();
2322
+ request.onerror = () => reject(request.error);
2323
+ request.onsuccess = () => resolve();
2324
+ }),
2325
+ new Promise((resolve, reject) => {
2326
+ const request = transaction.objectStore(ERRORS_STORE).clear();
2327
+ request.onerror = () => reject(request.error);
2328
+ request.onsuccess = () => resolve();
2329
+ })
2330
+ ]);
2331
+ if (this.debug) {
2332
+ console.log("[ExperienceSDK] Offline queue cleared");
2333
+ }
2334
+ } catch (error) {
2335
+ if (this.debug) {
2336
+ console.error("[ExperienceSDK] Failed to clear offline queue:", error);
2337
+ }
185
2338
  }
186
- });
187
- }
2339
+ }
2340
+ shutdown() {
2341
+ this.unsubscribeFns.forEach((fn) => fn());
2342
+ this.unsubscribeFns = [];
2343
+ if (this.db) {
2344
+ this.db.close();
2345
+ this.db = null;
2346
+ }
2347
+ this.isInitialized = false;
2348
+ }
2349
+ };
188
2350
 
189
2351
  // src/experience.ts
190
- var DEFAULT_CONFIG = {
191
- endpoint: "",
192
- enableTracking: true,
193
- enableErrorCapture: true,
194
- enableHeatmaps: false,
195
- enableBreadcrumbs: true,
196
- sampleRate: 1,
197
- batchIntervalMs: 5e3,
198
- maxBatchSize: 50,
199
- debug: false
200
- };
201
- var Experience = class _Experience {
2352
+ var _Experience = class _Experience {
202
2353
  constructor(config) {
203
- this.profileId = "";
204
- this.eventQueue = [];
205
- this.errorQueue = [];
206
- this.batchTimer = null;
207
- this.flags = {};
208
- this.flagsFetched = false;
209
- this.teardownFns = [];
210
- this.destroyed = false;
211
- this.config = { ...DEFAULT_CONFIG, ...config };
212
- this.breadcrumbs = new BreadcrumbManager();
213
- const stored = typeof sessionStorage !== "undefined" ? sessionStorage.getItem("exp_session_id") : null;
214
- if (stored) {
215
- this.sessionId = stored;
216
- } else {
217
- this.sessionId = generateUUID();
218
- if (typeof sessionStorage !== "undefined") {
219
- sessionStorage.setItem("exp_session_id", this.sessionId);
220
- }
2354
+ this.isShutdown = false;
2355
+ this.isInSample = true;
2356
+ this.config = this.resolveConfig(config);
2357
+ this.isInSample = Math.random() < this.config.sampleRate;
2358
+ if (this.config.debug) {
2359
+ console.log("[ExperienceSDK] Initializing with config:", this.config);
2360
+ console.log("[ExperienceSDK] In sample:", this.isInSample);
221
2361
  }
2362
+ this.sessionManager = new SessionManager();
2363
+ this.transport = new Transport({
2364
+ endpoint: this.config.endpoint,
2365
+ apiKey: this.config.apiKey,
2366
+ debug: this.config.debug
2367
+ });
2368
+ this.batcher = new EventBatcher(
2369
+ this.transport,
2370
+ this.config.batchIntervalMs,
2371
+ this.config.maxBatchSize,
2372
+ this.config.debug
2373
+ );
2374
+ this.breadcrumbManager = new BreadcrumbManager();
2375
+ this.heatmapTracker = new HeatmapTracker(this.batcher, this.sessionManager.getSessionId());
2376
+ this.errorCapture = new ErrorCapture(this.batcher, this.breadcrumbManager, this.sessionManager.getSessionId());
2377
+ this.autoTracker = new AutoTracker(this.batcher, this.sessionManager.getSessionId());
2378
+ this.dataTTracker = new DataTTracker(this.batcher, this.sessionManager.getSessionId());
2379
+ this.flagClient = new FeatureFlagClient(this.transport, this.config.appId, this.config.debug);
2380
+ this.offlineQueue = new OfflineQueue(this.transport, this.config.debug);
2381
+ this.setupFailureHandling();
2382
+ this.enableFeatures();
222
2383
  }
223
- static init(config) {
224
- const instance = new _Experience(config);
225
- instance.setup();
226
- return instance;
2384
+ resolveConfig(config) {
2385
+ return {
2386
+ appId: config.appId,
2387
+ apiKey: config.apiKey,
2388
+ endpoint: config.endpoint || "https://experience.shellapps.com",
2389
+ enableTracking: config.enableTracking ?? true,
2390
+ enableErrorCapture: config.enableErrorCapture ?? true,
2391
+ enableHeatmaps: config.enableHeatmaps ?? false,
2392
+ enableBreadcrumbs: config.enableBreadcrumbs ?? true,
2393
+ sampleRate: Math.max(0, Math.min(1, config.sampleRate ?? 1)),
2394
+ batchIntervalMs: config.batchIntervalMs ?? 2e3,
2395
+ maxBatchSize: config.maxBatchSize ?? 50,
2396
+ debug: config.debug ?? false
2397
+ };
227
2398
  }
228
- log(...args) {
229
- if (this.config.debug) {
230
- console.log("[Experience]", ...args);
231
- }
2399
+ setupFailureHandling() {
2400
+ const originalSendEventBatch = this.transport.sendEventBatch.bind(this.transport);
2401
+ const originalSendErrorBatch = this.transport.sendErrorBatch.bind(this.transport);
2402
+ this.transport.sendEventBatch = async (batch) => {
2403
+ try {
2404
+ return await originalSendEventBatch(batch);
2405
+ } catch (error) {
2406
+ await this.offlineQueue.storeEventBatch(batch);
2407
+ throw error;
2408
+ }
2409
+ };
2410
+ this.transport.sendErrorBatch = async (batch) => {
2411
+ try {
2412
+ return await originalSendErrorBatch(batch);
2413
+ } catch (error) {
2414
+ await this.offlineQueue.storeErrorBatch(batch);
2415
+ throw error;
2416
+ }
2417
+ };
232
2418
  }
233
- setup() {
234
- if (Math.random() > this.config.sampleRate) {
235
- this.log("Sampled out");
2419
+ enableFeatures() {
2420
+ if (!this.isInSample) {
2421
+ if (this.config.debug) {
2422
+ console.log("[ExperienceSDK] Not in sample, features disabled");
2423
+ }
236
2424
  return;
237
2425
  }
238
2426
  if (this.config.enableBreadcrumbs) {
239
- this.breadcrumbs.install();
2427
+ this.breadcrumbManager.enable();
240
2428
  }
241
2429
  if (this.config.enableErrorCapture) {
242
- this.setupErrorCapture();
2430
+ this.errorCapture.enable();
243
2431
  }
244
2432
  if (this.config.enableTracking) {
245
- this.setupAutoPageViews();
2433
+ this.autoTracker.enable();
2434
+ this.dataTTracker.enable();
246
2435
  }
247
2436
  if (this.config.enableHeatmaps) {
248
- this.setupHeatmaps();
249
- }
250
- this.batchTimer = setInterval(() => this.flush(), this.config.batchIntervalMs);
251
- const beforeUnload = () => this.flushSync();
252
- window.addEventListener("beforeunload", beforeUnload);
253
- this.teardownFns.push(() => window.removeEventListener("beforeunload", beforeUnload));
254
- const onlineHandler = () => {
255
- this.log("Back online, draining queue");
256
- drainOfflineQueue((url, body) => this.sendRequest(url, body)).catch(() => {
257
- });
258
- };
259
- window.addEventListener("online", onlineHandler);
260
- this.teardownFns.push(() => window.removeEventListener("online", onlineHandler));
261
- this.fetchFlags().catch(() => {
2437
+ this.heatmapTracker.enable();
2438
+ }
2439
+ this.flagClient.initialize().catch((error) => {
2440
+ if (this.config.debug) {
2441
+ console.error("[ExperienceSDK] Failed to initialize flags:", error);
2442
+ }
262
2443
  });
263
- this.log("Initialized", this.config.appId);
264
2444
  }
265
- // --- Public API ---
266
- track(eventName, metadata) {
267
- if (this.destroyed) return;
268
- this.enqueueEvent("custom", eventName, metadata);
2445
+ // Public API
2446
+ track(eventType, metadata) {
2447
+ if (!this.isInSample || this.isShutdown || !this.config.enableTracking) return;
2448
+ const pageContext = this.getCurrentPageContext();
2449
+ const trackEvent = {
2450
+ eventType,
2451
+ sessionId: this.sessionManager.getSessionId(),
2452
+ timestamp: Date.now(),
2453
+ metadata,
2454
+ pageContext
2455
+ };
2456
+ this.batcher.addTrackEvent(trackEvent);
2457
+ if (this.config.debug) {
2458
+ console.log("[ExperienceSDK] Event tracked:", trackEvent);
2459
+ }
269
2460
  }
270
2461
  trackPageView() {
271
- if (this.destroyed) return;
272
- this.enqueueEvent("page_view", "page_view");
273
- this.breadcrumbs.add({ type: "navigation", category: "page_view", message: location.href });
2462
+ this.autoTracker.trackCurrentPage();
274
2463
  }
275
- captureError(error, extra) {
276
- if (this.destroyed) return;
277
- this.enqueueError(error.message, error.stack || "", "error", extra);
2464
+ captureError(error, options) {
2465
+ if (!this.isInSample || this.isShutdown || !this.config.enableErrorCapture) return;
2466
+ this.errorCapture.captureException(error, options);
278
2467
  }
279
- captureMessage(msg, severity = "info") {
280
- if (this.destroyed) return;
281
- this.enqueueError(msg, "", severity);
2468
+ captureMessage(message, severity) {
2469
+ if (!this.isInSample || this.isShutdown || !this.config.enableErrorCapture) return;
2470
+ const sev = severity ? ErrorCapture.getSeverityFromString(severity) : 0 /* INFO */;
2471
+ this.errorCapture.captureMessage(message, sev);
282
2472
  }
283
- getFlag(name, defaultValue) {
284
- if (!this.flagsFetched || !(name in this.flags)) return defaultValue;
285
- return this.flags[name];
2473
+ getFlag(key, defaultValue) {
2474
+ if (this.isShutdown) return defaultValue;
2475
+ return this.flagClient.getFlag(key, defaultValue);
286
2476
  }
287
2477
  identify(profileId) {
288
- this.profileId = profileId;
289
- this.log("Identified", profileId);
2478
+ if (this.isShutdown) return;
2479
+ this.flagClient.setProfileId(profileId);
2480
+ if (this.config.debug) {
2481
+ console.log("[ExperienceSDK] Profile identified:", profileId);
2482
+ }
290
2483
  }
291
2484
  async shutdown() {
292
- if (this.destroyed) return;
293
- this.destroyed = true;
294
- if (this.batchTimer) {
295
- clearInterval(this.batchTimer);
296
- this.batchTimer = null;
297
- }
298
- await this.flush();
299
- this.breadcrumbs.teardown();
300
- this.teardownFns.forEach((fn) => fn());
301
- this.teardownFns = [];
302
- this.log("Shutdown complete");
303
- }
304
- // --- Internal ---
305
- enqueueEvent(eventType, eventName, metadata) {
306
- const event = {
307
- eventId: generateUUID(),
308
- appId: this.config.appId,
309
- profileId: this.profileId,
310
- sessionId: this.sessionId,
311
- timestampMs: Date.now(),
312
- eventType,
313
- pageUrl: location.href,
314
- elementTid: metadata?.elementTid || "",
315
- metadata: { eventName, ...metadata },
316
- pageContext: getPageContext(),
317
- elementContext: null
318
- };
319
- this.eventQueue.push(event);
320
- if (this.eventQueue.length >= this.config.maxBatchSize) {
321
- this.flush().catch(() => {
322
- });
2485
+ if (this.isShutdown) return;
2486
+ this.isShutdown = true;
2487
+ if (this.config.debug) {
2488
+ console.log("[ExperienceSDK] Shutting down...");
2489
+ }
2490
+ this.breadcrumbManager.disable();
2491
+ this.errorCapture.disable();
2492
+ this.heatmapTracker.disable();
2493
+ this.autoTracker.disable();
2494
+ this.dataTTracker.disable();
2495
+ await Promise.all([
2496
+ this.batcher.shutdown(),
2497
+ this.flagClient.shutdown(),
2498
+ this.offlineQueue.shutdown()
2499
+ ]);
2500
+ if (this.config.debug) {
2501
+ console.log("[ExperienceSDK] Shutdown complete");
323
2502
  }
324
2503
  }
325
- enqueueError(message, stack, severity, extra) {
326
- const report = {
327
- eventId: generateUUID(),
328
- appId: this.config.appId,
329
- profileId: this.profileId,
330
- sessionId: this.sessionId,
331
- timestampMs: Date.now(),
332
- message,
333
- stack,
334
- severity,
335
- extra: extra || {},
336
- pageContext: getPageContext(),
337
- breadcrumbs: this.breadcrumbs.getAll()
2504
+ // Utility methods
2505
+ getCurrentPageContext() {
2506
+ if (typeof window === "undefined" || typeof document === "undefined") {
2507
+ return {
2508
+ url: "unknown",
2509
+ title: "unknown",
2510
+ referrer: "",
2511
+ viewport: { width: 1920, height: 1080 }
2512
+ };
2513
+ }
2514
+ return {
2515
+ url: window.location.href,
2516
+ title: document.title || "",
2517
+ referrer: document.referrer || "",
2518
+ viewport: {
2519
+ width: window.innerWidth || 1920,
2520
+ height: window.innerHeight || 1080
2521
+ }
2522
+ };
2523
+ }
2524
+ // Debug methods
2525
+ getSessionInfo() {
2526
+ return {
2527
+ sessionId: this.sessionManager.getSessionId(),
2528
+ duration: this.sessionManager.getSessionDuration(),
2529
+ isInSample: this.isInSample
2530
+ };
2531
+ }
2532
+ async getQueueStatus() {
2533
+ const offlineStatus = await this.offlineQueue.getQueueStatus();
2534
+ return {
2535
+ pendingEvents: this.batcher.getPendingEventCount(),
2536
+ pendingErrors: this.batcher.getPendingErrorCount(),
2537
+ offlineQueue: offlineStatus
338
2538
  };
339
- this.errorQueue.push(report);
340
2539
  }
2540
+ getBreadcrumbs() {
2541
+ return this.breadcrumbManager.getBreadcrumbs();
2542
+ }
2543
+ getFlagCacheInfo() {
2544
+ return this.flagClient.getCacheInfo();
2545
+ }
2546
+ // Manual flush for testing
341
2547
  async flush() {
342
- if (this.eventQueue.length > 0) {
343
- const events = this.eventQueue.splice(0);
344
- const url = `${this.config.endpoint}/api/v1/ingest/events`;
345
- const body = JSON.stringify({ events });
346
- await this.sendOrQueue(url, body);
347
- }
348
- if (this.errorQueue.length > 0) {
349
- const errors = this.errorQueue.splice(0);
350
- const url = `${this.config.endpoint}/api/v1/ingest/errors`;
351
- const body = JSON.stringify({ errors });
352
- await this.sendOrQueue(url, body);
353
- }
354
- }
355
- flushSync() {
356
- if (typeof navigator.sendBeacon !== "function") return;
357
- if (this.eventQueue.length > 0) {
358
- const url = `${this.config.endpoint}/api/v1/ingest/events`;
359
- const blob = new Blob([JSON.stringify({ events: this.eventQueue.splice(0) })], { type: "application/json" });
360
- navigator.sendBeacon(url, blob);
361
- }
362
- if (this.errorQueue.length > 0) {
363
- const url = `${this.config.endpoint}/api/v1/ingest/errors`;
364
- const blob = new Blob([JSON.stringify({ errors: this.errorQueue.splice(0) })], { type: "application/json" });
365
- navigator.sendBeacon(url, blob);
366
- }
367
- }
368
- async sendOrQueue(url, body) {
369
- if (typeof navigator !== "undefined" && !navigator.onLine) {
370
- await enqueueOffline({ url, body });
371
- return;
372
- }
373
- try {
374
- await this.sendRequest(url, body);
375
- } catch {
376
- await enqueueOffline({ url, body }).catch(() => {
377
- });
378
- }
2548
+ if (this.isShutdown) return;
2549
+ await this.batcher.forceFlush();
2550
+ await this.offlineQueue.processStoredItems();
379
2551
  }
380
- async sendRequest(url, body) {
381
- const response = await fetch(url, {
382
- method: "POST",
383
- headers: {
384
- "Content-Type": "application/json",
385
- "Authorization": `Bearer ${this.config.apiKey}`
386
- },
387
- body
388
- });
389
- if (!response.ok) {
390
- throw new Error(`HTTP ${response.status}`);
2552
+ // Static factory method (singleton pattern)
2553
+ static init(config) {
2554
+ if (_Experience.instance) {
2555
+ if (config.debug) {
2556
+ console.warn("[ExperienceSDK] Already initialized, returning existing instance");
2557
+ }
2558
+ return _Experience.instance;
391
2559
  }
2560
+ _Experience.instance = new _Experience(config);
2561
+ return _Experience.instance;
392
2562
  }
393
- async fetchFlags() {
394
- try {
395
- const url = `${this.config.endpoint}/api/v1/flags/${this.config.appId}`;
396
- const response = await fetch(url, {
397
- headers: { "Authorization": `Bearer ${this.config.apiKey}` }
398
- });
399
- if (response.ok) {
400
- this.flags = await response.json();
401
- this.flagsFetched = true;
402
- this.log("Flags loaded", this.flags);
403
- }
404
- } catch {
405
- this.log("Failed to fetch flags");
406
- }
407
- }
408
- // --- Auto features ---
409
- setupErrorCapture() {
410
- const onError = (event) => {
411
- this.enqueueError(
412
- event.message,
413
- event.error?.stack || "",
414
- "error",
415
- { filename: event.filename, lineno: event.lineno, colno: event.colno }
416
- );
417
- };
418
- window.addEventListener("error", onError);
419
- this.teardownFns.push(() => window.removeEventListener("error", onError));
420
- const onUnhandledRejection = (event) => {
421
- const msg = event.reason instanceof Error ? event.reason.message : String(event.reason);
422
- const stack = event.reason instanceof Error ? event.reason.stack || "" : "";
423
- this.enqueueError(msg, stack, "error", { type: "unhandledrejection" });
424
- };
425
- window.addEventListener("unhandledrejection", onUnhandledRejection);
426
- this.teardownFns.push(() => window.removeEventListener("unhandledrejection", onUnhandledRejection));
427
- }
428
- setupAutoPageViews() {
429
- const originalPushState = history.pushState.bind(history);
430
- const originalReplaceState = history.replaceState.bind(history);
431
- history.pushState = (...args) => {
432
- originalPushState(...args);
433
- this.trackPageView();
2563
+ static getInstance() {
2564
+ return _Experience.instance;
2565
+ }
2566
+ // Advanced tracking methods
2567
+ trackElement(element, eventType) {
2568
+ if (!this.isInSample || this.isShutdown || !this.config.enableTracking) return;
2569
+ const elementContext = {
2570
+ selector: this.getElementSelector(element),
2571
+ tag: element.tagName.toLowerCase(),
2572
+ dataT: element.getAttribute("data-t") || void 0
434
2573
  };
435
- history.replaceState = (...args) => {
436
- originalReplaceState(...args);
437
- this.trackPageView();
2574
+ const trackEvent = {
2575
+ eventType,
2576
+ sessionId: this.sessionManager.getSessionId(),
2577
+ timestamp: Date.now(),
2578
+ pageContext: this.getCurrentPageContext(),
2579
+ elementContext
438
2580
  };
439
- this.teardownFns.push(() => {
440
- history.pushState = originalPushState;
441
- history.replaceState = originalReplaceState;
442
- });
443
- const onPopState = () => this.trackPageView();
444
- window.addEventListener("popstate", onPopState);
445
- this.teardownFns.push(() => window.removeEventListener("popstate", onPopState));
446
- this.trackPageView();
2581
+ this.batcher.addTrackEvent(trackEvent);
447
2582
  }
448
- setupHeatmaps() {
449
- const handleMove = throttle((e) => {
450
- this.enqueueEvent("heatmap_move", "mousemove", {
451
- x: e.clientX,
452
- y: e.clientY,
453
- viewportWidth: window.innerWidth,
454
- viewportHeight: window.innerHeight
455
- });
456
- }, 100);
457
- const handleClick = (e) => {
458
- const target = e.target;
459
- this.enqueueEvent("heatmap_click", "click", {
460
- x: e.clientX,
461
- y: e.clientY,
462
- viewportWidth: window.innerWidth,
463
- viewportHeight: window.innerHeight,
464
- tag: target?.tagName?.toLowerCase() || "",
465
- text: (target?.textContent || "").slice(0, 100)
466
- });
467
- };
468
- document.addEventListener("mousemove", handleMove);
469
- document.addEventListener("click", handleClick);
470
- this.teardownFns.push(() => {
471
- document.removeEventListener("mousemove", handleMove);
472
- document.removeEventListener("click", handleClick);
473
- });
2583
+ getElementSelector(element) {
2584
+ try {
2585
+ const tag = element.tagName.toLowerCase();
2586
+ const id = element.id ? `#${element.id}` : "";
2587
+ const className = element.className && typeof element.className === "string" ? `.${element.className.split(" ").filter((c) => c.trim()).join(".")}` : "";
2588
+ return `${tag}${id}${className}`.slice(0, 100);
2589
+ } catch {
2590
+ return element.tagName?.toLowerCase() || "unknown";
2591
+ }
474
2592
  }
475
2593
  };
2594
+ _Experience.instance = null;
2595
+ var Experience = _Experience;
476
2596
  // Annotate the CommonJS export names for ESM import in node:
477
2597
  0 && (module.exports = {
478
- Experience
2598
+ AutoTracker,
2599
+ BreadcrumbManager,
2600
+ DataTTracker,
2601
+ ErrorCapture,
2602
+ EventBatcher,
2603
+ Experience,
2604
+ FeatureFlagClient,
2605
+ HeatmapTracker,
2606
+ OfflineQueue,
2607
+ ProtoSchemas,
2608
+ ProtobufEncoder,
2609
+ SessionManager,
2610
+ Severity,
2611
+ Transport
479
2612
  });
480
2613
  //# sourceMappingURL=index.js.map