@syntrologie/runtime-sdk 2.10.0 → 2.11.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.
@@ -9,6 +9,9 @@ var SyntrologieSDK = (() => {
9
9
  throw TypeError(msg);
10
10
  };
11
11
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __esm = (fn2, res) => function __init() {
13
+ return fn2 && (res = (0, fn2[__getOwnPropNames(fn2)[0]])(fn2 = 0)), res;
14
+ };
12
15
  var __commonJS = (cb, mod) => function __require() {
13
16
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
17
  };
@@ -12761,6 +12764,40 @@ var SyntrologieSDK = (() => {
12761
12764
  }
12762
12765
  });
12763
12766
 
12767
+ // src/telemetry/replayMirror.ts
12768
+ var replayMirror_exports = {};
12769
+ __export(replayMirror_exports, {
12770
+ setupReplayMirror: () => setupReplayMirror
12771
+ });
12772
+ function setupReplayMirror(options2) {
12773
+ const { posthogHost, mirrorEndpoint, sessionIdHeader = "X-Syntro-Session-Id" } = options2;
12774
+ const recordingPath = `${posthogHost}/s/`;
12775
+ const originalFetch = globalThis.fetch.bind(globalThis);
12776
+ globalThis.fetch = (input, init2) => {
12777
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
12778
+ const result = originalFetch(input, init2);
12779
+ if (url.startsWith(recordingPath)) {
12780
+ originalFetch(mirrorEndpoint, {
12781
+ method: init2?.method ?? "POST",
12782
+ body: init2?.body,
12783
+ headers: {
12784
+ ...init2?.headers,
12785
+ [sessionIdHeader]: "syntro-session"
12786
+ }
12787
+ }).catch(() => {
12788
+ });
12789
+ }
12790
+ return result;
12791
+ };
12792
+ return () => {
12793
+ globalThis.fetch = originalFetch;
12794
+ };
12795
+ }
12796
+ var init_replayMirror = __esm({
12797
+ "src/telemetry/replayMirror.ts"() {
12798
+ }
12799
+ });
12800
+
12764
12801
  // src/index.ts
12765
12802
  var index_exports = {};
12766
12803
  __export(index_exports, {
@@ -12847,7 +12884,7 @@ var SyntrologieSDK = (() => {
12847
12884
  SmartCanvasController: () => SmartCanvasController,
12848
12885
  SmartCanvasElement: () => SmartCanvasElement,
12849
12886
  SmartCanvasPortal: () => SmartCanvasPortal,
12850
- StandardEvents: () => StandardEvents,
12887
+ StandardEvents: () => StandardEvents2,
12851
12888
  StateEqualsConditionZ: () => StateEqualsConditionZ,
12852
12889
  StateStore: () => StateStore,
12853
12890
  StoredValueZ: () => StoredValueZ,
@@ -21565,7 +21602,7 @@ ${text2}</tr>
21565
21602
  }
21566
21603
 
21567
21604
  // src/version.ts
21568
- var SDK_VERSION = "2.10.0";
21605
+ var SDK_VERSION = "2.11.0";
21569
21606
 
21570
21607
  // src/types.ts
21571
21608
  var SDK_SCHEMA_VERSION = "2.0";
@@ -22365,8 +22402,522 @@ ${text2}</tr>
22365
22402
  var import_react14 = __toESM(require_react(), 1);
22366
22403
  var import_react_dom = __toESM(require_react_dom(), 1);
22367
22404
 
22368
- // src/events/types.ts
22405
+ // ../event-processor/dist/types.js
22406
+ var EVENT_SCHEMA_VERSION = "1.0.0";
22369
22407
  var StandardEvents = {
22408
+ UI_CLICK: "ui.click",
22409
+ UI_SCROLL: "ui.scroll",
22410
+ UI_INPUT: "ui.input",
22411
+ UI_CHANGE: "ui.change",
22412
+ UI_SUBMIT: "ui.submit",
22413
+ NAV_PAGE_VIEW: "nav.page_view",
22414
+ NAV_PAGE_LEAVE: "nav.page_leave",
22415
+ UI_HESITATE: "ui.hesitate",
22416
+ UI_RAGE_CLICK: "ui.rage_click",
22417
+ UI_SCROLL_THRASH: "ui.scroll_thrash",
22418
+ UI_FOCUS_BOUNCE: "ui.focus_bounce",
22419
+ UI_IDLE: "ui.idle",
22420
+ UI_HOVER: "ui.hover"
22421
+ };
22422
+ var RRWebSource = {
22423
+ Mutation: 0,
22424
+ MouseMove: 1,
22425
+ MouseInteraction: 2,
22426
+ Scroll: 3,
22427
+ ViewportResize: 4,
22428
+ Input: 5,
22429
+ TouchMove: 6,
22430
+ MediaInteraction: 7,
22431
+ Drag: 12
22432
+ };
22433
+ var RRWebMouseInteraction = {
22434
+ MouseUp: 0,
22435
+ MouseDown: 1,
22436
+ Click: 2,
22437
+ ContextMenu: 3,
22438
+ DblClick: 4,
22439
+ Focus: 5,
22440
+ Blur: 6,
22441
+ TouchStart: 7,
22442
+ TouchEnd: 9
22443
+ };
22444
+ var DEFAULT_DETECTOR_CONFIG = {
22445
+ hesitationMs: 3e3,
22446
+ hesitationRadiusPx: 10,
22447
+ rageClickCount: 3,
22448
+ rageClickWindowMs: 1e3,
22449
+ rageClickRadiusPx: 30,
22450
+ scrollThrashReversals: 3,
22451
+ scrollThrashWindowMs: 2e3,
22452
+ focusBounceMaxInputs: 0,
22453
+ idleMs: 5e3,
22454
+ hoverSampleMs: 100
22455
+ };
22456
+
22457
+ // ../event-processor/dist/detectors/focus-bounce.js
22458
+ var FocusBounceDetector = class {
22459
+ constructor(config, emit) {
22460
+ this.config = config;
22461
+ this.emit = emit;
22462
+ this.focused = /* @__PURE__ */ new Map();
22463
+ }
22464
+ ingest(raw) {
22465
+ if (raw.type !== 3)
22466
+ return;
22467
+ const ts2 = raw.timestamp;
22468
+ if (raw.data.source === RRWebSource.MouseInteraction) {
22469
+ const id = raw.data.id ?? 0;
22470
+ if (raw.data.type === RRWebMouseInteraction.Focus) {
22471
+ this.focused.set(id, { id, focusTs: ts2, inputCount: 0 });
22472
+ } else if (raw.data.type === RRWebMouseInteraction.Blur) {
22473
+ const entry = this.focused.get(id);
22474
+ if (entry && entry.inputCount <= this.config.focusBounceMaxInputs) {
22475
+ this.emit({
22476
+ ts: ts2,
22477
+ name: StandardEvents.UI_FOCUS_BOUNCE,
22478
+ source: "rrweb",
22479
+ schemaVersion: EVENT_SCHEMA_VERSION,
22480
+ props: {
22481
+ elementId: id,
22482
+ duration_ms: ts2 - entry.focusTs
22483
+ }
22484
+ });
22485
+ }
22486
+ this.focused.delete(id);
22487
+ }
22488
+ } else if (raw.data.source === RRWebSource.Input) {
22489
+ const id = raw.data.id ?? 0;
22490
+ const entry = this.focused.get(id);
22491
+ if (entry) {
22492
+ entry.inputCount++;
22493
+ }
22494
+ }
22495
+ }
22496
+ };
22497
+
22498
+ // ../event-processor/dist/detectors/hesitation.js
22499
+ var HesitationDetector = class {
22500
+ constructor(config, emit) {
22501
+ this.config = config;
22502
+ this.emit = emit;
22503
+ this.anchorX = 0;
22504
+ this.anchorY = 0;
22505
+ this.anchorTs = 0;
22506
+ this.emitted = false;
22507
+ this.hasPosition = false;
22508
+ }
22509
+ ingest(raw) {
22510
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
22511
+ return;
22512
+ const positions = raw.data.positions;
22513
+ if (!positions || positions.length === 0)
22514
+ return;
22515
+ const last = positions[positions.length - 1];
22516
+ const ts2 = raw.timestamp + (last.timeOffset ?? 0);
22517
+ if (!this.hasPosition) {
22518
+ this.anchorX = last.x;
22519
+ this.anchorY = last.y;
22520
+ this.anchorTs = ts2;
22521
+ this.hasPosition = true;
22522
+ this.emitted = false;
22523
+ return;
22524
+ }
22525
+ const dx = last.x - this.anchorX;
22526
+ const dy = last.y - this.anchorY;
22527
+ const dist = Math.sqrt(dx * dx + dy * dy);
22528
+ if (dist > this.config.hesitationRadiusPx) {
22529
+ this.anchorX = last.x;
22530
+ this.anchorY = last.y;
22531
+ this.anchorTs = ts2;
22532
+ this.emitted = false;
22533
+ }
22534
+ }
22535
+ tick(now) {
22536
+ if (!this.hasPosition || this.emitted)
22537
+ return;
22538
+ const elapsed = now - this.anchorTs;
22539
+ if (elapsed >= this.config.hesitationMs) {
22540
+ this.emit({
22541
+ ts: now,
22542
+ name: StandardEvents.UI_HESITATE,
22543
+ source: "rrweb",
22544
+ schemaVersion: EVENT_SCHEMA_VERSION,
22545
+ props: {
22546
+ x: this.anchorX,
22547
+ y: this.anchorY,
22548
+ duration_ms: elapsed
22549
+ }
22550
+ });
22551
+ this.emitted = true;
22552
+ }
22553
+ }
22554
+ };
22555
+
22556
+ // ../event-processor/dist/detectors/hover.js
22557
+ var HoverTracker = class {
22558
+ constructor(config, emit, elementResolver) {
22559
+ this.config = config;
22560
+ this.emit = emit;
22561
+ this.elementResolver = elementResolver;
22562
+ this.currentElement = null;
22563
+ this.hoverStartTs = null;
22564
+ this.lastX = 0;
22565
+ this.lastY = 0;
22566
+ this.lastSampleTs = -Infinity;
22567
+ }
22568
+ ingest(raw) {
22569
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
22570
+ return;
22571
+ const positions = raw.data.positions;
22572
+ if (!positions || positions.length === 0)
22573
+ return;
22574
+ const last = positions[positions.length - 1];
22575
+ this.lastX = last.x;
22576
+ this.lastY = last.y;
22577
+ }
22578
+ tick(now) {
22579
+ if (!this.elementResolver)
22580
+ return;
22581
+ if (now - this.lastSampleTs < this.config.hoverSampleMs)
22582
+ return;
22583
+ this.lastSampleTs = now;
22584
+ const newElement = this.elementResolver(this.lastX, this.lastY);
22585
+ const newKey = newElement ? elementKey(newElement) : null;
22586
+ const currentKey = this.currentElement ? elementKey(this.currentElement) : null;
22587
+ if (newKey !== currentKey) {
22588
+ if (this.currentElement && this.hoverStartTs !== null) {
22589
+ this.emit({
22590
+ ts: now,
22591
+ name: StandardEvents.UI_HOVER,
22592
+ source: "rrweb",
22593
+ schemaVersion: EVENT_SCHEMA_VERSION,
22594
+ props: {
22595
+ x: this.lastX,
22596
+ y: this.lastY,
22597
+ duration_ms: now - this.hoverStartTs,
22598
+ element: this.currentElement
22599
+ }
22600
+ });
22601
+ }
22602
+ this.currentElement = newElement;
22603
+ this.hoverStartTs = now;
22604
+ }
22605
+ }
22606
+ };
22607
+ function elementKey(el) {
22608
+ return `${el.tag_name ?? ""}|${el.attr__id ?? ""}|${(el.classes ?? []).join(",")}`;
22609
+ }
22610
+
22611
+ // ../event-processor/dist/detectors/idle.js
22612
+ var IdleDetector = class {
22613
+ constructor(config, emit) {
22614
+ this.config = config;
22615
+ this.emit = emit;
22616
+ this.lastActivityTs = null;
22617
+ this.emitted = false;
22618
+ }
22619
+ ingest(raw) {
22620
+ if (raw.type !== 3)
22621
+ return;
22622
+ const src = raw.data.source;
22623
+ if (src === RRWebSource.MouseMove || src === RRWebSource.MouseInteraction || src === RRWebSource.Scroll) {
22624
+ this.lastActivityTs = raw.timestamp;
22625
+ this.emitted = false;
22626
+ }
22627
+ }
22628
+ tick(now) {
22629
+ if (this.lastActivityTs === null || this.emitted)
22630
+ return;
22631
+ if (now - this.lastActivityTs >= this.config.idleMs) {
22632
+ this.emit({
22633
+ ts: now,
22634
+ name: StandardEvents.UI_IDLE,
22635
+ source: "rrweb",
22636
+ schemaVersion: EVENT_SCHEMA_VERSION,
22637
+ props: {
22638
+ idle_ms: now - this.lastActivityTs
22639
+ }
22640
+ });
22641
+ this.emitted = true;
22642
+ }
22643
+ }
22644
+ };
22645
+
22646
+ // ../event-processor/dist/detectors/rage-click.js
22647
+ var RageClickDetector = class {
22648
+ constructor(config, emit) {
22649
+ this.config = config;
22650
+ this.emit = emit;
22651
+ this.clicks = [];
22652
+ }
22653
+ ingest(raw) {
22654
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseInteraction)
22655
+ return;
22656
+ if (raw.data.type !== RRWebMouseInteraction.Click)
22657
+ return;
22658
+ const x2 = raw.data.x ?? 0;
22659
+ const y2 = raw.data.y ?? 0;
22660
+ const ts2 = raw.timestamp;
22661
+ const cutoff = ts2 - this.config.rageClickWindowMs;
22662
+ this.clicks = this.clicks.filter((c2) => c2.ts >= cutoff);
22663
+ this.clicks.push({ ts: ts2, x: x2, y: y2 });
22664
+ const nearby = this.clicks.filter((c2) => {
22665
+ const dx = c2.x - x2;
22666
+ const dy = c2.y - y2;
22667
+ return Math.sqrt(dx * dx + dy * dy) <= this.config.rageClickRadiusPx;
22668
+ });
22669
+ if (nearby.length >= this.config.rageClickCount) {
22670
+ this.emit({
22671
+ ts: ts2,
22672
+ name: StandardEvents.UI_RAGE_CLICK,
22673
+ source: "rrweb",
22674
+ schemaVersion: EVENT_SCHEMA_VERSION,
22675
+ props: {
22676
+ x: x2,
22677
+ y: y2,
22678
+ clickCount: nearby.length,
22679
+ duration_ms: ts2 - nearby[0].ts
22680
+ }
22681
+ });
22682
+ this.clicks = [];
22683
+ }
22684
+ }
22685
+ };
22686
+
22687
+ // ../event-processor/dist/detectors/scroll-thrash.js
22688
+ var ScrollThrashDetector = class {
22689
+ constructor(config, emit) {
22690
+ this.config = config;
22691
+ this.emit = emit;
22692
+ this.lastY = null;
22693
+ this.lastDirection = null;
22694
+ this.reversals = [];
22695
+ }
22696
+ ingest(raw) {
22697
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.Scroll)
22698
+ return;
22699
+ const y2 = raw.data.y ?? 0;
22700
+ const ts2 = raw.timestamp;
22701
+ if (this.lastY !== null) {
22702
+ const direction = y2 > this.lastY ? "down" : y2 < this.lastY ? "up" : this.lastDirection;
22703
+ if (direction && direction !== this.lastDirection) {
22704
+ const cutoff = ts2 - this.config.scrollThrashWindowMs;
22705
+ this.reversals = this.reversals.filter((t2) => t2 > cutoff);
22706
+ this.reversals.push(ts2);
22707
+ if (this.reversals.length >= this.config.scrollThrashReversals) {
22708
+ this.emit({
22709
+ ts: ts2,
22710
+ name: StandardEvents.UI_SCROLL_THRASH,
22711
+ source: "rrweb",
22712
+ schemaVersion: EVENT_SCHEMA_VERSION,
22713
+ props: {
22714
+ reversals: this.reversals.length,
22715
+ duration_ms: ts2 - this.reversals[0]
22716
+ }
22717
+ });
22718
+ this.reversals = [];
22719
+ }
22720
+ }
22721
+ this.lastDirection = direction;
22722
+ }
22723
+ this.lastY = y2;
22724
+ }
22725
+ };
22726
+
22727
+ // ../event-processor/dist/normalizers/posthog.js
22728
+ var POSTHOG_EVENT_MAP = {
22729
+ // NOTE: $autocapture is intentionally NOT in this map.
22730
+ // It's handled below in getEventName() with $event_type refinement
22731
+ // so that change/submit events aren't all mapped to ui.click.
22732
+ $click: StandardEvents.UI_CLICK,
22733
+ $scroll: StandardEvents.UI_SCROLL,
22734
+ $input: StandardEvents.UI_INPUT,
22735
+ $change: StandardEvents.UI_CHANGE,
22736
+ $submit: StandardEvents.UI_SUBMIT,
22737
+ // Navigation events
22738
+ $pageview: StandardEvents.NAV_PAGE_VIEW,
22739
+ $pageleave: StandardEvents.NAV_PAGE_LEAVE,
22740
+ // Session events
22741
+ $session_start: "session.start",
22742
+ // Identify events
22743
+ $identify: "user.identify"
22744
+ };
22745
+ function getEventName(phEvent) {
22746
+ const eventName = phEvent.event;
22747
+ if (typeof eventName !== "string") {
22748
+ return "posthog.unknown";
22749
+ }
22750
+ if (POSTHOG_EVENT_MAP[eventName]) {
22751
+ return POSTHOG_EVENT_MAP[eventName];
22752
+ }
22753
+ if (eventName === "$autocapture") {
22754
+ const tagName = phEvent.properties?.$tag_name;
22755
+ const eventType = phEvent.properties?.$event_type;
22756
+ if (eventType === "submit")
22757
+ return StandardEvents.UI_SUBMIT;
22758
+ if (eventType === "change")
22759
+ return StandardEvents.UI_CHANGE;
22760
+ if (tagName === "input" || tagName === "textarea")
22761
+ return StandardEvents.UI_INPUT;
22762
+ return StandardEvents.UI_CLICK;
22763
+ }
22764
+ if (!eventName.startsWith("$")) {
22765
+ return `posthog.${eventName}`;
22766
+ }
22767
+ return eventName.replace("$", "posthog.");
22768
+ }
22769
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea"]);
22770
+ function resolveInteractiveTag(elements2, directTag) {
22771
+ if (directTag && INTERACTIVE_TAGS.has(directTag))
22772
+ return directTag;
22773
+ if (!elements2)
22774
+ return directTag;
22775
+ for (const el of elements2) {
22776
+ const tag3 = el.tag_name;
22777
+ if (tag3 && INTERACTIVE_TAGS.has(tag3))
22778
+ return tag3;
22779
+ }
22780
+ return directTag;
22781
+ }
22782
+ function extractProps(phEvent) {
22783
+ const props = {};
22784
+ const phProps = phEvent.properties || {};
22785
+ const elements2 = phProps.$elements;
22786
+ const directTag = phProps.$tag_name ?? elements2?.[0]?.tag_name;
22787
+ const isClickEvent = phEvent.event === "$autocapture" || phEvent.event === "$click";
22788
+ props.tagName = isClickEvent ? resolveInteractiveTag(elements2, directTag) : directTag;
22789
+ if (phProps.$el_text)
22790
+ props.elementText = phProps.$el_text;
22791
+ if (elements2)
22792
+ props.elements = elements2;
22793
+ if (phProps.$current_url)
22794
+ props.url = phProps.$current_url;
22795
+ if (phProps.$pathname)
22796
+ props.pathname = phProps.$pathname;
22797
+ if (phProps.$host)
22798
+ props.host = phProps.$host;
22799
+ if (phProps.$viewport_width)
22800
+ props.viewportWidth = phProps.$viewport_width;
22801
+ if (phProps.$viewport_height)
22802
+ props.viewportHeight = phProps.$viewport_height;
22803
+ if (phProps.$session_id)
22804
+ props.sessionId = phProps.$session_id;
22805
+ if (phProps.$scroll_depth)
22806
+ props.scrollDepth = phProps.$scroll_depth;
22807
+ if (phProps.$scroll_percentage)
22808
+ props.scrollPercentage = phProps.$scroll_percentage;
22809
+ props.originalEvent = phEvent.event;
22810
+ return props;
22811
+ }
22812
+ function normalizePostHogEvent(phEvent) {
22813
+ let ts2;
22814
+ if (typeof phEvent.timestamp === "number") {
22815
+ ts2 = phEvent.timestamp;
22816
+ } else if (typeof phEvent.timestamp === "string") {
22817
+ ts2 = new Date(phEvent.timestamp).getTime();
22818
+ } else {
22819
+ ts2 = Date.now();
22820
+ }
22821
+ return {
22822
+ ts: ts2,
22823
+ name: getEventName(phEvent),
22824
+ source: "posthog",
22825
+ props: extractProps(phEvent),
22826
+ schemaVersion: EVENT_SCHEMA_VERSION
22827
+ };
22828
+ }
22829
+ function shouldNormalizeEvent(phEvent) {
22830
+ const eventName = phEvent.event;
22831
+ if (typeof eventName !== "string")
22832
+ return false;
22833
+ const skipEvents = [
22834
+ "$feature_flag_called",
22835
+ "$feature_flags",
22836
+ "$groups",
22837
+ "$groupidentify",
22838
+ "$set",
22839
+ "$set_once",
22840
+ "$unset",
22841
+ "$create_alias",
22842
+ "$capture_metrics",
22843
+ "$performance_event",
22844
+ "$web_vitals",
22845
+ "$exception",
22846
+ "$dead_click",
22847
+ "$heatmap"
22848
+ ];
22849
+ if (skipEvents.includes(eventName)) {
22850
+ return false;
22851
+ }
22852
+ return true;
22853
+ }
22854
+ function createPostHogNormalizer(publishFn) {
22855
+ return (eventName, properties) => {
22856
+ if (typeof eventName !== "string")
22857
+ return;
22858
+ const phEvent = {
22859
+ event: eventName,
22860
+ properties,
22861
+ timestamp: Date.now()
22862
+ };
22863
+ if (shouldNormalizeEvent(phEvent)) {
22864
+ const normalizedEvent = normalizePostHogEvent(phEvent);
22865
+ publishFn(normalizedEvent);
22866
+ }
22867
+ };
22868
+ }
22869
+
22870
+ // ../event-processor/dist/processor.js
22871
+ function createEventProcessor(options2) {
22872
+ const config = { ...DEFAULT_DETECTOR_CONFIG, ...options2?.config };
22873
+ const listeners = [];
22874
+ function emit(event) {
22875
+ for (const cb of listeners)
22876
+ cb(event);
22877
+ }
22878
+ const hesitation = new HesitationDetector(config, emit);
22879
+ const rageClick = new RageClickDetector(config, emit);
22880
+ const scrollThrash = new ScrollThrashDetector(config, emit);
22881
+ const focusBounce = new FocusBounceDetector(config, emit);
22882
+ const idle = new IdleDetector(config, emit);
22883
+ const hover = new HoverTracker(config, emit, options2?.elementResolver);
22884
+ const clickAttrBuffer = [];
22885
+ return {
22886
+ ingest(raw) {
22887
+ if (raw.kind === "posthog") {
22888
+ const { kind: _2, ...phEvent } = raw;
22889
+ if (shouldNormalizeEvent(phEvent)) {
22890
+ emit(normalizePostHogEvent(phEvent));
22891
+ }
22892
+ } else if (raw.kind === "rrweb") {
22893
+ hesitation.ingest(raw);
22894
+ rageClick.ingest(raw);
22895
+ scrollThrash.ingest(raw);
22896
+ focusBounce.ingest(raw);
22897
+ idle.ingest(raw);
22898
+ hover.ingest(raw);
22899
+ }
22900
+ },
22901
+ onEvent(callback) {
22902
+ listeners.push(callback);
22903
+ },
22904
+ tick(timestamp) {
22905
+ hesitation.tick(timestamp);
22906
+ idle.tick(timestamp);
22907
+ hover.tick(timestamp);
22908
+ },
22909
+ enrichClickAttributes(timestamp, elements2) {
22910
+ clickAttrBuffer.push({ ts: timestamp, elements: elements2 });
22911
+ const cutoff = timestamp - 500;
22912
+ while (clickAttrBuffer.length > 0 && clickAttrBuffer[0].ts < cutoff) {
22913
+ clickAttrBuffer.shift();
22914
+ }
22915
+ }
22916
+ };
22917
+ }
22918
+
22919
+ // src/events/types.ts
22920
+ var StandardEvents2 = {
22370
22921
  // UI events (from PostHog autocapture)
22371
22922
  UI_CLICK: "ui.click",
22372
22923
  UI_SCROLL: "ui.scroll",
@@ -22406,7 +22957,6 @@ ${text2}</tr>
22406
22957
  SURFACE_MOUNTED: "surface.mounted",
22407
22958
  SURFACE_UNMOUNTED: "surface.unmounted"
22408
22959
  };
22409
- var EVENT_SCHEMA_VERSION = "1.0.0";
22410
22960
 
22411
22961
  // src/events/normalizers/canvas.ts
22412
22962
  function createCanvasEvent(name, props) {
@@ -22419,48 +22969,48 @@ ${text2}</tr>
22419
22969
  };
22420
22970
  }
22421
22971
  function canvasOpened(surface) {
22422
- return createCanvasEvent(StandardEvents.CANVAS_OPENED, { surface });
22972
+ return createCanvasEvent(StandardEvents2.CANVAS_OPENED, { surface });
22423
22973
  }
22424
22974
  function canvasClosed(surface) {
22425
- return createCanvasEvent(StandardEvents.CANVAS_CLOSED, { surface });
22975
+ return createCanvasEvent(StandardEvents2.CANVAS_CLOSED, { surface });
22426
22976
  }
22427
22977
  function tileViewed(tileId, surface) {
22428
- return createCanvasEvent(StandardEvents.TILE_VIEWED, { tileId, surface });
22978
+ return createCanvasEvent(StandardEvents2.TILE_VIEWED, { tileId, surface });
22429
22979
  }
22430
22980
  function tileExpanded(tileId, surface) {
22431
- return createCanvasEvent(StandardEvents.TILE_EXPANDED, { tileId, surface });
22981
+ return createCanvasEvent(StandardEvents2.TILE_EXPANDED, { tileId, surface });
22432
22982
  }
22433
22983
  function tileCollapsed(tileId, surface) {
22434
- return createCanvasEvent(StandardEvents.TILE_COLLAPSED, { tileId, surface });
22984
+ return createCanvasEvent(StandardEvents2.TILE_COLLAPSED, { tileId, surface });
22435
22985
  }
22436
22986
  function tileAction(tileId, actionId, surface) {
22437
- return createCanvasEvent(StandardEvents.TILE_ACTION, {
22987
+ return createCanvasEvent(StandardEvents2.TILE_ACTION, {
22438
22988
  tileId,
22439
22989
  actionId,
22440
22990
  surface
22441
22991
  });
22442
22992
  }
22443
22993
  function overlayStarted(recipeId, recipeName) {
22444
- return createCanvasEvent(StandardEvents.OVERLAY_STARTED, {
22994
+ return createCanvasEvent(StandardEvents2.OVERLAY_STARTED, {
22445
22995
  recipeId,
22446
22996
  recipeName
22447
22997
  });
22448
22998
  }
22449
22999
  function overlayCompleted(recipeId, recipeName) {
22450
- return createCanvasEvent(StandardEvents.OVERLAY_COMPLETED, {
23000
+ return createCanvasEvent(StandardEvents2.OVERLAY_COMPLETED, {
22451
23001
  recipeId,
22452
23002
  recipeName
22453
23003
  });
22454
23004
  }
22455
23005
  function overlayDismissed(recipeId, recipeName, stepIndex) {
22456
- return createCanvasEvent(StandardEvents.OVERLAY_DISMISSED, {
23006
+ return createCanvasEvent(StandardEvents2.OVERLAY_DISMISSED, {
22457
23007
  recipeId,
22458
23008
  recipeName,
22459
23009
  stepIndex
22460
23010
  });
22461
23011
  }
22462
23012
  function overlayStepViewed(recipeId, stepIndex, stepTitle) {
22463
- return createCanvasEvent(StandardEvents.OVERLAY_STEP_VIEWED, {
23013
+ return createCanvasEvent(StandardEvents2.OVERLAY_STEP_VIEWED, {
22464
23014
  recipeId,
22465
23015
  stepIndex,
22466
23016
  stepTitle
@@ -23109,7 +23659,7 @@ ${text2}</tr>
23109
23659
  const timerIds = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
23110
23660
  const publishDismissed = (0, import_react9.useCallback)(
23111
23661
  (notif) => {
23112
- eventBus?.publish(StandardEvents.NOTIFICATION_DISMISSED, {
23662
+ eventBus?.publish(StandardEvents2.NOTIFICATION_DISMISSED, {
23113
23663
  notificationId: notif.id,
23114
23664
  tileId: notif.tileId,
23115
23665
  itemId: notif.itemId
@@ -23173,7 +23723,7 @@ ${text2}</tr>
23173
23723
  }
23174
23724
  return next;
23175
23725
  });
23176
- eventBus.publish(StandardEvents.NOTIFICATION_SHOWN, {
23726
+ eventBus.publish(StandardEvents2.NOTIFICATION_SHOWN, {
23177
23727
  notificationId: matched.id,
23178
23728
  tileId: matched.tileId,
23179
23729
  itemId: matched.itemId,
@@ -23980,7 +24530,7 @@ ${cssRules}
23980
24530
  const handleNotificationClick = (0, import_react14.useCallback)(
23981
24531
  (notif) => {
23982
24532
  if (runtime7) {
23983
- runtime7.events.publish(StandardEvents.NOTIFICATION_CLICKED, {
24533
+ runtime7.events.publish(StandardEvents2.NOTIFICATION_CLICKED, {
23984
24534
  notificationId: notif.id,
23985
24535
  tileId: notif.tileId,
23986
24536
  itemId: notif.itemId
@@ -23990,7 +24540,7 @@ ${cssRules}
23990
24540
  onToggle();
23991
24541
  }
23992
24542
  if (runtime7 && notif.tileId) {
23993
- runtime7.events.publish(StandardEvents.NOTIFICATION_DEEP_LINK, {
24543
+ runtime7.events.publish(StandardEvents2.NOTIFICATION_DEEP_LINK, {
23994
24544
  tileId: notif.tileId,
23995
24545
  itemId: notif.itemId
23996
24546
  });
@@ -36959,10 +37509,12 @@ ${cssRules}
36959
37509
  __publicField(this, "client");
36960
37510
  __publicField(this, "featureFlagsCallback");
36961
37511
  __publicField(this, "captureCallback");
37512
+ __publicField(this, "rrwebCallback");
36962
37513
  __publicField(this, "consentUnsub");
36963
37514
  this.client = options2.client;
36964
37515
  this.featureFlagsCallback = options2.onFeatureFlagsLoaded;
36965
37516
  this.captureCallback = options2.onCapture;
37517
+ this.rrwebCallback = options2.onRRWebEvent;
36966
37518
  if (!this.client && options2.consent && options2.requireExplicitConsent && typeof window !== "undefined" && options2.apiKey) {
36967
37519
  const consent = options2.consent;
36968
37520
  const currentStatus = consent.getStatus();
@@ -37047,6 +37599,59 @@ ${cssRules}
37047
37599
  },
37048
37600
  instanceName
37049
37601
  );
37602
+ if (this.rrwebCallback && this.client) {
37603
+ this.setupRRWebIntercept();
37604
+ }
37605
+ if (options2.replayMirrorEndpoint && this.client) {
37606
+ Promise.resolve().then(() => (init_replayMirror(), replayMirror_exports)).then(({ setupReplayMirror: setupReplayMirror2 }) => {
37607
+ setupReplayMirror2({
37608
+ posthogHost: options2.apiHost ?? "https://telemetry.syntrologie.com",
37609
+ mirrorEndpoint: options2.replayMirrorEndpoint
37610
+ });
37611
+ });
37612
+ }
37613
+ }
37614
+ /**
37615
+ * Set up rrweb event interception on PostHog's session recording.
37616
+ *
37617
+ * PostHog lazy-loads the rrweb recorder. The SessionRecording wrapper has
37618
+ * an `onRRwebEmit` method, but rrweb delivers events directly to the
37619
+ * lazy-loaded recorder instance's `onRRwebEmit`, bypassing the wrapper.
37620
+ * We must find and patch the recorder instance, not the wrapper.
37621
+ *
37622
+ * The recorder instance is stored on a minified property of SessionRecording.
37623
+ * We detect it by looking for an object with both `onRRwebEmit` and `start` methods.
37624
+ */
37625
+ setupRRWebIntercept(retries = 30) {
37626
+ const sr2 = this.client?.sessionRecording;
37627
+ if (!sr2) {
37628
+ if (retries > 0) {
37629
+ setTimeout(() => this.setupRRWebIntercept(retries - 1), 500);
37630
+ }
37631
+ return;
37632
+ }
37633
+ let recorder = null;
37634
+ for (const key of Object.getOwnPropertyNames(sr2)) {
37635
+ const val = sr2[key];
37636
+ if (val && typeof val === "object" && typeof val.onRRwebEmit === "function" && typeof val.start === "function" && val !== sr2) {
37637
+ recorder = val;
37638
+ break;
37639
+ }
37640
+ }
37641
+ if (!recorder) {
37642
+ if (retries > 0) {
37643
+ setTimeout(() => this.setupRRWebIntercept(retries - 1), 500);
37644
+ }
37645
+ return;
37646
+ }
37647
+ const originalEmit = recorder.onRRwebEmit.bind(recorder);
37648
+ recorder.onRRwebEmit = (rawEvent) => {
37649
+ this.rrwebCallback?.({ kind: "rrweb", ...rawEvent });
37650
+ originalEmit(rawEvent);
37651
+ };
37652
+ if (typeof window !== "undefined") {
37653
+ window.__RRWEB_INTERCEPT_READY__ = true;
37654
+ }
37050
37655
  }
37051
37656
  /**
37052
37657
  * Get all feature flags from PostHog.
@@ -39742,131 +40347,6 @@ ${cssRules}
39742
40347
  return new EventBus(options2);
39743
40348
  }
39744
40349
 
39745
- // src/events/normalizers/posthog.ts
39746
- var POSTHOG_EVENT_MAP = {
39747
- // NOTE: $autocapture is intentionally NOT in this map.
39748
- // It's handled below in getEventName() with $event_type refinement
39749
- // so that change/submit events aren't all mapped to ui.click.
39750
- $click: StandardEvents.UI_CLICK,
39751
- $scroll: StandardEvents.UI_SCROLL,
39752
- $input: StandardEvents.UI_INPUT,
39753
- $change: StandardEvents.UI_CHANGE,
39754
- $submit: StandardEvents.UI_SUBMIT,
39755
- // Navigation events
39756
- $pageview: StandardEvents.NAV_PAGE_VIEW,
39757
- $pageleave: StandardEvents.NAV_PAGE_LEAVE,
39758
- // Session events
39759
- $session_start: "session.start",
39760
- // Identify events
39761
- $identify: "user.identify"
39762
- };
39763
- function getEventName(phEvent) {
39764
- const eventName = phEvent.event;
39765
- if (typeof eventName !== "string") {
39766
- return "posthog.unknown";
39767
- }
39768
- if (POSTHOG_EVENT_MAP[eventName]) {
39769
- return POSTHOG_EVENT_MAP[eventName];
39770
- }
39771
- if (eventName === "$autocapture") {
39772
- const tagName = phEvent.properties?.$tag_name;
39773
- const eventType = phEvent.properties?.$event_type;
39774
- if (eventType === "submit") return StandardEvents.UI_SUBMIT;
39775
- if (eventType === "change") return StandardEvents.UI_CHANGE;
39776
- if (tagName === "input" || tagName === "textarea") return StandardEvents.UI_INPUT;
39777
- return StandardEvents.UI_CLICK;
39778
- }
39779
- if (!eventName.startsWith("$")) {
39780
- return `posthog.${eventName}`;
39781
- }
39782
- return eventName.replace("$", "posthog.");
39783
- }
39784
- var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea"]);
39785
- function resolveInteractiveTag(elements2, directTag) {
39786
- if (directTag && INTERACTIVE_TAGS.has(directTag)) return directTag;
39787
- if (!elements2) return directTag;
39788
- for (const el of elements2) {
39789
- const tag3 = el.tag_name;
39790
- if (tag3 && INTERACTIVE_TAGS.has(tag3)) return tag3;
39791
- }
39792
- return directTag;
39793
- }
39794
- function extractProps(phEvent) {
39795
- const props = {};
39796
- const phProps = phEvent.properties || {};
39797
- const elements2 = phProps.$elements;
39798
- const directTag = phProps.$tag_name ?? elements2?.[0]?.tag_name;
39799
- const isClickEvent = phEvent.event === "$autocapture" || phEvent.event === "$click";
39800
- props.tagName = isClickEvent ? resolveInteractiveTag(elements2, directTag) : directTag;
39801
- if (phProps.$el_text) props.elementText = phProps.$el_text;
39802
- if (elements2) props.elements = elements2;
39803
- if (phProps.$current_url) props.url = phProps.$current_url;
39804
- if (phProps.$pathname) props.pathname = phProps.$pathname;
39805
- if (phProps.$host) props.host = phProps.$host;
39806
- if (phProps.$viewport_width) props.viewportWidth = phProps.$viewport_width;
39807
- if (phProps.$viewport_height) props.viewportHeight = phProps.$viewport_height;
39808
- if (phProps.$session_id) props.sessionId = phProps.$session_id;
39809
- if (phProps.$scroll_depth) props.scrollDepth = phProps.$scroll_depth;
39810
- if (phProps.$scroll_percentage) props.scrollPercentage = phProps.$scroll_percentage;
39811
- props.originalEvent = phEvent.event;
39812
- return props;
39813
- }
39814
- function normalizePostHogEvent(phEvent) {
39815
- let ts2;
39816
- if (typeof phEvent.timestamp === "number") {
39817
- ts2 = phEvent.timestamp;
39818
- } else if (typeof phEvent.timestamp === "string") {
39819
- ts2 = new Date(phEvent.timestamp).getTime();
39820
- } else {
39821
- ts2 = Date.now();
39822
- }
39823
- return {
39824
- ts: ts2,
39825
- name: getEventName(phEvent),
39826
- source: "posthog",
39827
- props: extractProps(phEvent),
39828
- schemaVersion: EVENT_SCHEMA_VERSION
39829
- };
39830
- }
39831
- function shouldNormalizeEvent(phEvent) {
39832
- const eventName = phEvent.event;
39833
- if (typeof eventName !== "string") return false;
39834
- const skipEvents = [
39835
- "$feature_flag_called",
39836
- "$feature_flags",
39837
- "$groups",
39838
- "$groupidentify",
39839
- "$set",
39840
- "$set_once",
39841
- "$unset",
39842
- "$create_alias",
39843
- "$capture_metrics",
39844
- "$performance_event",
39845
- "$web_vitals",
39846
- "$exception",
39847
- "$dead_click",
39848
- "$heatmap"
39849
- ];
39850
- if (skipEvents.includes(eventName)) {
39851
- return false;
39852
- }
39853
- return true;
39854
- }
39855
- function createPostHogNormalizer(publishFn) {
39856
- return (eventName, properties) => {
39857
- if (typeof eventName !== "string") return;
39858
- const phEvent = {
39859
- event: eventName,
39860
- properties,
39861
- timestamp: Date.now()
39862
- };
39863
- if (shouldNormalizeEvent(phEvent)) {
39864
- const normalizedEvent = normalizePostHogEvent(phEvent);
39865
- publishFn(normalizedEvent);
39866
- }
39867
- };
39868
- }
39869
-
39870
40350
  // src/events/schema.ts
39871
40351
  var EventSourceZ = external_exports.enum(["posthog", "canvas", "derived"]);
39872
40352
  var NormalizedEventZ = external_exports.object({
@@ -41574,9 +42054,44 @@ ${cssRules}
41574
42054
  let experiments;
41575
42055
  const events = createEventBus();
41576
42056
  console.log("[Syntro Bootstrap] EventBus created");
41577
- const postHogNormalizer = createPostHogNormalizer((event) => {
41578
- events.publishEvent(event);
42057
+ const processor = createEventProcessor({
42058
+ elementResolver: typeof window !== "undefined" ? (x2, y2) => {
42059
+ const el = document.elementFromPoint(x2, y2);
42060
+ if (!el) return null;
42061
+ const info = { tag_name: el.tagName.toLowerCase() };
42062
+ if (el.id) info.attr__id = el.id;
42063
+ if (el.className && typeof el.className === "string") {
42064
+ info.classes = el.className.split(" ").filter(Boolean);
42065
+ }
42066
+ for (const attr of el.attributes) {
42067
+ if (attr.name.startsWith("data-")) info[`attr__${attr.name}`] = attr.value;
42068
+ }
42069
+ return info;
42070
+ } : void 0
41579
42071
  });
42072
+ processor.onEvent((event) => events.publishEvent(event));
42073
+ if (typeof window !== "undefined") {
42074
+ setInterval(() => processor.tick(Date.now()), 1e3);
42075
+ document.addEventListener(
42076
+ "click",
42077
+ (e2) => {
42078
+ const chain = [];
42079
+ let el = e2.target;
42080
+ while (el && el !== document.body) {
42081
+ const info = { tag_name: el.tagName.toLowerCase() };
42082
+ for (const attr of el.attributes) {
42083
+ if (attr.name.startsWith("data-") || attr.name === "id" || attr.name === "class" || attr.name === "aria-label") {
42084
+ info[`attr__${attr.name}`] = attr.value;
42085
+ }
42086
+ }
42087
+ chain.push(info);
42088
+ el = el.parentElement;
42089
+ }
42090
+ processor.enrichClickAttributes(Date.now(), chain);
42091
+ },
42092
+ true
42093
+ );
42094
+ }
41580
42095
  const onFeatureFlagsLoaded = (allFlags) => {
41581
42096
  debug("Syntro Bootstrap", "Phase 2: PostHog feature flags loaded");
41582
42097
  const segmentFlags = extractSegmentFlags(allFlags);
@@ -41600,8 +42115,14 @@ ${cssRules}
41600
42115
  enableFeatureFlags: true,
41601
42116
  // Wire up callback for when flags are loaded (Phase 2)
41602
42117
  onFeatureFlagsLoaded,
41603
- // Wire up event capture to feed into EventBus
41604
- onCapture: postHogNormalizer
42118
+ // Wire up event capture to feed into event processor
42119
+ onCapture: (eventName, properties) => {
42120
+ processor.ingest({ kind: "posthog", event: eventName, properties, timestamp: Date.now() });
42121
+ },
42122
+ // Wire rrweb events for behavioral signal detection
42123
+ onRRWebEvent: (event) => {
42124
+ processor.ingest(event);
42125
+ }
41605
42126
  });
41606
42127
  console.log(`[Syntro Bootstrap] Telemetry client created (${provider}) with EventBus wiring`);
41607
42128
  }
@@ -41792,7 +42313,7 @@ ${cssRules}
41792
42313
  }
41793
42314
 
41794
42315
  // src/index.ts
41795
- var RUNTIME_SDK_BUILD = true ? `${"2026-03-14T02:31:26.217Z"} (${"7fefac724c"})` : "dev";
42316
+ var RUNTIME_SDK_BUILD = true ? `${"2026-03-14T03:04:52.784Z"} (${"5e67d79633"})` : "dev";
41796
42317
  if (typeof window !== "undefined") {
41797
42318
  console.log(`[Syntro Runtime] Build: ${RUNTIME_SDK_BUILD}`);
41798
42319
  const existing = window.SynOS;