@syntrologie/runtime-sdk 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CAPABILITIES.md +176 -117
  2. package/README.md +2 -0
  3. package/dist/actions/schema.d.ts +7 -7
  4. package/dist/actions/schema.js +3 -4
  5. package/dist/actions/types.d.ts +1 -1
  6. package/dist/actions/validation-core.d.ts +24 -0
  7. package/dist/actions/validation-rules.d.ts +74 -0
  8. package/dist/actions/validation.d.ts +5 -11
  9. package/dist/bootstrap-init.d.ts +33 -0
  10. package/dist/bootstrap-runtime.d.ts +7 -0
  11. package/dist/bootstrap-types.d.ts +90 -0
  12. package/dist/bootstrap.d.ts +17 -83
  13. package/dist/{chunk-Q77NT67W.js → chunk-BU4Z6PD7.js} +16 -1
  14. package/dist/{chunk-Q77NT67W.js.map → chunk-BU4Z6PD7.js.map} +1 -1
  15. package/dist/{chunk-H3FAYTUV.js → chunk-J2LGX2PV.js} +1216 -394
  16. package/dist/chunk-J2LGX2PV.js.map +7 -0
  17. package/dist/{chunk-NBFQGKSV.js → chunk-L6RJMBR2.js} +4 -4
  18. package/dist/{chunk-NBFQGKSV.js.map → chunk-L6RJMBR2.js.map} +2 -2
  19. package/dist/{chunk-37TTQRH5.js → chunk-XDYJ64IN.js} +2 -2
  20. package/dist/config/schema.js +2 -3
  21. package/dist/decisions/schema.js +1 -2
  22. package/dist/events/EventBus.d.ts +27 -1
  23. package/dist/events/history.d.ts +9 -0
  24. package/dist/events/index.d.ts +3 -0
  25. package/dist/events/types.d.ts +24 -0
  26. package/dist/events/validation.d.ts +7 -0
  27. package/dist/index.d.ts +0 -2
  28. package/dist/index.js +1131 -2039
  29. package/dist/index.js.map +4 -4
  30. package/dist/overlays/runtime/overlay/overlay-runner.d.ts +4 -0
  31. package/dist/overlays/runtime/overlay/overlay-state.d.ts +21 -0
  32. package/dist/overlays/types.d.ts +3 -1
  33. package/dist/react.js +6 -5
  34. package/dist/react.js.map +2 -2
  35. package/dist/smart-canvas.esm.js +92 -108
  36. package/dist/smart-canvas.esm.js.map +4 -4
  37. package/dist/smart-canvas.js +4649 -4957
  38. package/dist/smart-canvas.js.map +4 -4
  39. package/dist/smart-canvas.min.js +92 -108
  40. package/dist/smart-canvas.min.js.map +4 -4
  41. package/dist/telemetry/adapters/posthog.d.ts +5 -10
  42. package/dist/test/setup.d.ts +1 -0
  43. package/dist/token.d.ts +2 -0
  44. package/dist/version.d.ts +1 -1
  45. package/package.json +23 -29
  46. package/schema/canvas-config.schema.json +1 -1
  47. package/scripts/syntroReactPlugin.mjs +3 -0
  48. package/scripts/validate-config.mjs +42 -0
  49. package/dist/chunk-H3FAYTUV.js.map +0 -7
  50. package/dist/chunk-JMHRHAEL.js +0 -18
  51. package/dist/chunk-JMHRHAEL.js.map +0 -7
  52. package/dist/replayMirror-QZ3GQ527.js +0 -32
  53. package/dist/replayMirror-QZ3GQ527.js.map +0 -7
  54. package/dist/telemetry/replayMirror.d.ts +0 -7
  55. /package/dist/{chunk-37TTQRH5.js.map → chunk-XDYJ64IN.js.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  __privateGet,
4
4
  __privateSet,
5
5
  __publicField
6
- } from "./chunk-JMHRHAEL.js";
6
+ } from "./chunk-BU4Z6PD7.js";
7
7
 
8
8
  // ../adaptives/adaptive-content/dist/reconciliation-guard.js
9
9
  function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
@@ -127,6 +127,12 @@ function sanitizeHtml(html) {
127
127
  }
128
128
  }
129
129
  }
130
+ const svgs = Array.from(root.querySelectorAll("svg"));
131
+ for (const svg of svgs) {
132
+ if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === "script")) {
133
+ toRemove.push(svg);
134
+ }
135
+ }
130
136
  for (const el of toRemove) {
131
137
  while (el.firstChild) {
132
138
  (_a2 = el.parentNode) == null ? void 0 : _a2.insertBefore(el.firstChild, el);
@@ -755,6 +761,10 @@ var CelebrationEngine = class {
755
761
  this.container = null;
756
762
  }
757
763
  start(container, effect, config) {
764
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
765
+ if (prefersReducedMotion) {
766
+ return;
767
+ }
758
768
  this.container = container;
759
769
  this.effect = effect;
760
770
  this.duration = config.duration;
@@ -1398,7 +1408,7 @@ var alert = {
1398
1408
  var tag = {
1399
1409
  content: slateGrey[10],
1400
1410
  border: slateGrey[4],
1401
- background: "#1c2124"
1411
+ background: slateGrey[3]
1402
1412
  };
1403
1413
  var menu = {
1404
1414
  backgroundDefault: slateGrey[2],
@@ -2283,7 +2293,7 @@ function extractWorkflowsFromActive(activeActions) {
2283
2293
  const workflows = /* @__PURE__ */ new Map();
2284
2294
  for (const entry of activeActions) {
2285
2295
  const action = entry.action;
2286
- if (action.kind === "core:tour" && action.workflow && action.tourId) {
2296
+ if (action.kind === "overlays:tour" && action.workflow && action.tourId) {
2287
2297
  const meta = action.workflow;
2288
2298
  const rawSteps = action.steps || [];
2289
2299
  const steps = rawSteps.map((s) => {
@@ -2806,7 +2816,7 @@ var executors2 = [
2806
2816
  { kind: "overlays:badge", executor: executeBadge },
2807
2817
  { kind: "overlays:tooltip", executor: executeTooltip },
2808
2818
  { kind: "overlays:modal", executor: executeModal },
2809
- { kind: "core:tour", executor: executeTour },
2819
+ { kind: "overlays:tour", executor: executeTour },
2810
2820
  { kind: "overlays:celebrate", executor: executeCelebrate }
2811
2821
  ];
2812
2822
  var runtime2 = {
@@ -2926,7 +2936,15 @@ var CORE_ACTION_KINDS = /* @__PURE__ */ new Set([
2926
2936
  "core:wait",
2927
2937
  "core:sequence",
2928
2938
  "core:parallel",
2929
- "core:tour"
2939
+ "overlays:tour"
2940
+ ]);
2941
+ var BUNDLED_APP_IDS = /* @__PURE__ */ new Set([
2942
+ "adaptive-chatbot",
2943
+ "adaptive-content",
2944
+ "adaptive-faq",
2945
+ "adaptive-gamification",
2946
+ "adaptive-nav",
2947
+ "adaptive-overlays"
2930
2948
  ]);
2931
2949
  var NAMESPACE_TO_APP_ID = {
2932
2950
  faq: "adaptive-faq",
@@ -3080,6 +3098,9 @@ function createAppLoader(options) {
3080
3098
  }
3081
3099
  }
3082
3100
  }
3101
+ for (const bundled of BUNDLED_APP_IDS) {
3102
+ appIds.delete(bundled);
3103
+ }
3083
3104
  return Array.from(appIds);
3084
3105
  }
3085
3106
  async function loadAppsForConfig(config) {
@@ -3435,7 +3456,7 @@ function getAntiFlickerSnippet(config = {}) {
3435
3456
  }
3436
3457
 
3437
3458
  // src/version.ts
3438
- var SDK_VERSION = "2.11.0";
3459
+ var SDK_VERSION = "2.12.0";
3439
3460
 
3440
3461
  // src/types.ts
3441
3462
  var SDK_SCHEMA_VERSION = "2.0";
@@ -3586,12 +3607,15 @@ function getCachedConfig(sdkVersion) {
3586
3607
  var resolveConfigUri = ({
3587
3608
  configUri,
3588
3609
  experiments,
3589
- featureKey = "smart-canvas-config-uri"
3610
+ featureKey
3590
3611
  }) => {
3591
3612
  var _a2;
3592
3613
  if (configUri) return configUri;
3593
- const fromFeature = (_a2 = experiments == null ? void 0 : experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, featureKey, null);
3594
- if (fromFeature) return fromFeature;
3614
+ if (experiments) {
3615
+ const key = featureKey != null ? featureKey : "smart-canvas-config";
3616
+ const fromFeature = (_a2 = experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, key, null);
3617
+ if (fromFeature) return fromFeature;
3618
+ }
3595
3619
  return void 0;
3596
3620
  };
3597
3621
  function getVariantFlagKeys(experiments, manifestKey, variantFlagPrefix) {
@@ -3617,14 +3641,15 @@ var createCanvasConfigFetcher = ({
3617
3641
  experiments,
3618
3642
  featureKey,
3619
3643
  credentials,
3620
- configFeatureKey = "smart-canvas-config",
3644
+ configFeatureKey,
3621
3645
  manifestKey,
3622
3646
  variantFlagPrefix,
3623
3647
  sdkVersion
3624
3648
  }) => async () => {
3625
3649
  var _a2;
3626
- if (experiments && configFeatureKey) {
3627
- const directConfig = (_a2 = experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, configFeatureKey, null);
3650
+ const effectiveConfigKey = configFeatureKey != null ? configFeatureKey : "smart-canvas-config";
3651
+ if (experiments) {
3652
+ const directConfig = (_a2 = experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, effectiveConfigKey, null);
3628
3653
  if (directConfig && typeof directConfig === "object") {
3629
3654
  debug("SmartCanvas Config", "Resolved config directly from feature flag", directConfig);
3630
3655
  return directConfig;
@@ -3812,9 +3837,529 @@ function useShadowRoot() {
3812
3837
  return ctx;
3813
3838
  }
3814
3839
 
3815
- // src/events/types.ts
3816
- import { EVENT_SCHEMA_VERSION } from "@syntrologie/event-processor";
3840
+ // ../event-processor/dist/types.js
3841
+ var EVENT_SCHEMA_VERSION = "1.0.0";
3817
3842
  var StandardEvents = {
3843
+ UI_CLICK: "ui.click",
3844
+ UI_SCROLL: "ui.scroll",
3845
+ UI_INPUT: "ui.input",
3846
+ UI_CHANGE: "ui.change",
3847
+ UI_SUBMIT: "ui.submit",
3848
+ NAV_PAGE_VIEW: "nav.page_view",
3849
+ NAV_PAGE_LEAVE: "nav.page_leave",
3850
+ UI_HESITATE: "ui.hesitate",
3851
+ UI_RAGE_CLICK: "ui.rage_click",
3852
+ UI_SCROLL_THRASH: "ui.scroll_thrash",
3853
+ UI_FOCUS_BOUNCE: "ui.focus_bounce",
3854
+ UI_IDLE: "ui.idle",
3855
+ UI_HOVER: "ui.hover"
3856
+ };
3857
+ var RRWebSource = {
3858
+ Mutation: 0,
3859
+ MouseMove: 1,
3860
+ MouseInteraction: 2,
3861
+ Scroll: 3,
3862
+ ViewportResize: 4,
3863
+ Input: 5,
3864
+ TouchMove: 6,
3865
+ MediaInteraction: 7,
3866
+ Drag: 12
3867
+ };
3868
+ var RRWebMouseInteraction = {
3869
+ MouseUp: 0,
3870
+ MouseDown: 1,
3871
+ Click: 2,
3872
+ ContextMenu: 3,
3873
+ DblClick: 4,
3874
+ Focus: 5,
3875
+ Blur: 6,
3876
+ TouchStart: 7,
3877
+ TouchEnd: 9
3878
+ };
3879
+ var DEFAULT_DETECTOR_CONFIG = {
3880
+ hesitationMs: 3e3,
3881
+ hesitationRadiusPx: 10,
3882
+ rageClickCount: 3,
3883
+ rageClickWindowMs: 1e3,
3884
+ rageClickRadiusPx: 30,
3885
+ scrollThrashReversals: 3,
3886
+ scrollThrashWindowMs: 2e3,
3887
+ focusBounceMaxInputs: 0,
3888
+ idleMs: 5e3,
3889
+ hoverSampleMs: 100
3890
+ };
3891
+
3892
+ // ../event-processor/dist/normalizers/posthog.js
3893
+ var POSTHOG_EVENT_MAP = {
3894
+ // NOTE: $autocapture is intentionally NOT in this map.
3895
+ // It's handled below in getEventName() with $event_type refinement
3896
+ // so that change/submit events aren't all mapped to ui.click.
3897
+ $click: StandardEvents.UI_CLICK,
3898
+ $scroll: StandardEvents.UI_SCROLL,
3899
+ $input: StandardEvents.UI_INPUT,
3900
+ $change: StandardEvents.UI_CHANGE,
3901
+ $submit: StandardEvents.UI_SUBMIT,
3902
+ // Navigation events
3903
+ $pageview: StandardEvents.NAV_PAGE_VIEW,
3904
+ $pageleave: StandardEvents.NAV_PAGE_LEAVE,
3905
+ // Session events
3906
+ $session_start: "session.start",
3907
+ // Identify events
3908
+ $identify: "user.identify"
3909
+ };
3910
+ function getEventName(phEvent) {
3911
+ var _a2, _b;
3912
+ const eventName = phEvent.event;
3913
+ if (typeof eventName !== "string") {
3914
+ return "posthog.unknown";
3915
+ }
3916
+ if (POSTHOG_EVENT_MAP[eventName]) {
3917
+ return POSTHOG_EVENT_MAP[eventName];
3918
+ }
3919
+ if (eventName === "$autocapture") {
3920
+ const tagName = (_a2 = phEvent.properties) == null ? void 0 : _a2.$tag_name;
3921
+ const eventType = (_b = phEvent.properties) == null ? void 0 : _b.$event_type;
3922
+ if (eventType === "submit")
3923
+ return StandardEvents.UI_SUBMIT;
3924
+ if (eventType === "change")
3925
+ return StandardEvents.UI_CHANGE;
3926
+ if (tagName === "input" || tagName === "textarea")
3927
+ return StandardEvents.UI_INPUT;
3928
+ return StandardEvents.UI_CLICK;
3929
+ }
3930
+ if (!eventName.startsWith("$")) {
3931
+ return `posthog.${eventName}`;
3932
+ }
3933
+ return eventName.replace("$", "posthog.");
3934
+ }
3935
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea"]);
3936
+ function resolveInteractiveTag(elements, directTag) {
3937
+ if (directTag && INTERACTIVE_TAGS.has(directTag))
3938
+ return directTag;
3939
+ if (!elements)
3940
+ return directTag;
3941
+ for (const el of elements) {
3942
+ const tag2 = el.tag_name;
3943
+ if (tag2 && INTERACTIVE_TAGS.has(tag2))
3944
+ return tag2;
3945
+ }
3946
+ return directTag;
3947
+ }
3948
+ function extractProps(phEvent) {
3949
+ var _a2, _b;
3950
+ const props = {};
3951
+ const phProps = phEvent.properties || {};
3952
+ const elements = phProps.$elements;
3953
+ const directTag = (_b = phProps.$tag_name) != null ? _b : (_a2 = elements == null ? void 0 : elements[0]) == null ? void 0 : _a2.tag_name;
3954
+ const isClickEvent = phEvent.event === "$autocapture" || phEvent.event === "$click";
3955
+ props.tagName = isClickEvent ? resolveInteractiveTag(elements, directTag) : directTag;
3956
+ if (phProps.$el_text)
3957
+ props.elementText = phProps.$el_text;
3958
+ if (elements)
3959
+ props.elements = elements;
3960
+ if (phProps.$current_url)
3961
+ props.url = phProps.$current_url;
3962
+ if (phProps.$pathname)
3963
+ props.pathname = phProps.$pathname;
3964
+ if (phProps.$host)
3965
+ props.host = phProps.$host;
3966
+ if (phProps.$viewport_width)
3967
+ props.viewportWidth = phProps.$viewport_width;
3968
+ if (phProps.$viewport_height)
3969
+ props.viewportHeight = phProps.$viewport_height;
3970
+ if (phProps.$session_id)
3971
+ props.sessionId = phProps.$session_id;
3972
+ if (phProps.$scroll_depth)
3973
+ props.scrollDepth = phProps.$scroll_depth;
3974
+ if (phProps.$scroll_percentage)
3975
+ props.scrollPercentage = phProps.$scroll_percentage;
3976
+ props.originalEvent = phEvent.event;
3977
+ return props;
3978
+ }
3979
+ function normalizePostHogEvent(phEvent) {
3980
+ let ts;
3981
+ if (typeof phEvent.timestamp === "number") {
3982
+ ts = phEvent.timestamp;
3983
+ } else if (typeof phEvent.timestamp === "string") {
3984
+ ts = new Date(phEvent.timestamp).getTime();
3985
+ } else {
3986
+ ts = Date.now();
3987
+ }
3988
+ return {
3989
+ ts,
3990
+ name: getEventName(phEvent),
3991
+ source: "posthog",
3992
+ props: extractProps(phEvent),
3993
+ schemaVersion: EVENT_SCHEMA_VERSION
3994
+ };
3995
+ }
3996
+ function shouldNormalizeEvent(phEvent) {
3997
+ const eventName = phEvent.event;
3998
+ if (typeof eventName !== "string")
3999
+ return false;
4000
+ const skipEvents = [
4001
+ "$feature_flag_called",
4002
+ "$feature_flags",
4003
+ "$groups",
4004
+ "$groupidentify",
4005
+ "$set",
4006
+ "$set_once",
4007
+ "$unset",
4008
+ "$create_alias",
4009
+ "$capture_metrics",
4010
+ "$performance_event",
4011
+ "$web_vitals",
4012
+ "$exception",
4013
+ "$dead_click",
4014
+ "$heatmap"
4015
+ ];
4016
+ if (skipEvents.includes(eventName)) {
4017
+ return false;
4018
+ }
4019
+ return true;
4020
+ }
4021
+ function createPostHogNormalizer(publishFn) {
4022
+ return (eventName, properties) => {
4023
+ if (typeof eventName !== "string")
4024
+ return;
4025
+ const phEvent = {
4026
+ event: eventName,
4027
+ properties,
4028
+ timestamp: Date.now()
4029
+ };
4030
+ if (shouldNormalizeEvent(phEvent)) {
4031
+ const normalizedEvent = normalizePostHogEvent(phEvent);
4032
+ publishFn(normalizedEvent);
4033
+ }
4034
+ };
4035
+ }
4036
+
4037
+ // ../event-processor/dist/detectors/focus-bounce.js
4038
+ var FocusBounceDetector = class {
4039
+ constructor(config, emit2) {
4040
+ this.config = config;
4041
+ this.emit = emit2;
4042
+ this.focused = /* @__PURE__ */ new Map();
4043
+ }
4044
+ ingest(raw) {
4045
+ var _a2, _b;
4046
+ if (raw.type !== 3)
4047
+ return;
4048
+ const ts = raw.timestamp;
4049
+ if (raw.data.source === RRWebSource.MouseInteraction) {
4050
+ const id = (_a2 = raw.data.id) != null ? _a2 : 0;
4051
+ if (raw.data.type === RRWebMouseInteraction.Focus) {
4052
+ this.focused.set(id, { id, focusTs: ts, inputCount: 0 });
4053
+ } else if (raw.data.type === RRWebMouseInteraction.Blur) {
4054
+ const entry = this.focused.get(id);
4055
+ if (entry && entry.inputCount <= this.config.focusBounceMaxInputs) {
4056
+ this.emit({
4057
+ ts,
4058
+ name: StandardEvents.UI_FOCUS_BOUNCE,
4059
+ source: "rrweb",
4060
+ schemaVersion: EVENT_SCHEMA_VERSION,
4061
+ props: {
4062
+ elementId: id,
4063
+ duration_ms: ts - entry.focusTs
4064
+ }
4065
+ });
4066
+ }
4067
+ this.focused.delete(id);
4068
+ }
4069
+ } else if (raw.data.source === RRWebSource.Input) {
4070
+ const id = (_b = raw.data.id) != null ? _b : 0;
4071
+ const entry = this.focused.get(id);
4072
+ if (entry) {
4073
+ entry.inputCount++;
4074
+ }
4075
+ }
4076
+ }
4077
+ };
4078
+
4079
+ // ../event-processor/dist/detectors/hesitation.js
4080
+ var HesitationDetector = class {
4081
+ constructor(config, emit2) {
4082
+ this.config = config;
4083
+ this.emit = emit2;
4084
+ this.anchorX = 0;
4085
+ this.anchorY = 0;
4086
+ this.anchorTs = 0;
4087
+ this.emitted = false;
4088
+ this.hasPosition = false;
4089
+ }
4090
+ ingest(raw) {
4091
+ var _a2;
4092
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
4093
+ return;
4094
+ const positions = raw.data.positions;
4095
+ if (!positions || positions.length === 0)
4096
+ return;
4097
+ const last = positions[positions.length - 1];
4098
+ const ts = raw.timestamp + ((_a2 = last.timeOffset) != null ? _a2 : 0);
4099
+ if (!this.hasPosition) {
4100
+ this.anchorX = last.x;
4101
+ this.anchorY = last.y;
4102
+ this.anchorTs = ts;
4103
+ this.hasPosition = true;
4104
+ this.emitted = false;
4105
+ return;
4106
+ }
4107
+ const dx = last.x - this.anchorX;
4108
+ const dy = last.y - this.anchorY;
4109
+ const dist = Math.sqrt(dx * dx + dy * dy);
4110
+ if (dist > this.config.hesitationRadiusPx) {
4111
+ this.anchorX = last.x;
4112
+ this.anchorY = last.y;
4113
+ this.anchorTs = ts;
4114
+ this.emitted = false;
4115
+ }
4116
+ }
4117
+ tick(now) {
4118
+ if (!this.hasPosition || this.emitted)
4119
+ return;
4120
+ const elapsed = now - this.anchorTs;
4121
+ if (elapsed >= this.config.hesitationMs) {
4122
+ this.emit({
4123
+ ts: now,
4124
+ name: StandardEvents.UI_HESITATE,
4125
+ source: "rrweb",
4126
+ schemaVersion: EVENT_SCHEMA_VERSION,
4127
+ props: {
4128
+ x: this.anchorX,
4129
+ y: this.anchorY,
4130
+ duration_ms: elapsed
4131
+ }
4132
+ });
4133
+ this.emitted = true;
4134
+ }
4135
+ }
4136
+ };
4137
+
4138
+ // ../event-processor/dist/detectors/hover.js
4139
+ var HoverTracker = class {
4140
+ constructor(config, emit2, elementResolver) {
4141
+ this.config = config;
4142
+ this.emit = emit2;
4143
+ this.elementResolver = elementResolver;
4144
+ this.currentElement = null;
4145
+ this.hoverStartTs = null;
4146
+ this.lastX = 0;
4147
+ this.lastY = 0;
4148
+ this.lastSampleTs = -Infinity;
4149
+ }
4150
+ ingest(raw) {
4151
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
4152
+ return;
4153
+ const positions = raw.data.positions;
4154
+ if (!positions || positions.length === 0)
4155
+ return;
4156
+ const last = positions[positions.length - 1];
4157
+ this.lastX = last.x;
4158
+ this.lastY = last.y;
4159
+ }
4160
+ tick(now) {
4161
+ if (!this.elementResolver)
4162
+ return;
4163
+ if (now - this.lastSampleTs < this.config.hoverSampleMs)
4164
+ return;
4165
+ this.lastSampleTs = now;
4166
+ const newElement = this.elementResolver(this.lastX, this.lastY);
4167
+ const newKey = newElement ? elementKey(newElement) : null;
4168
+ const currentKey = this.currentElement ? elementKey(this.currentElement) : null;
4169
+ if (newKey !== currentKey) {
4170
+ if (this.currentElement && this.hoverStartTs !== null) {
4171
+ this.emit({
4172
+ ts: now,
4173
+ name: StandardEvents.UI_HOVER,
4174
+ source: "rrweb",
4175
+ schemaVersion: EVENT_SCHEMA_VERSION,
4176
+ props: {
4177
+ x: this.lastX,
4178
+ y: this.lastY,
4179
+ duration_ms: now - this.hoverStartTs,
4180
+ element: this.currentElement
4181
+ }
4182
+ });
4183
+ }
4184
+ this.currentElement = newElement;
4185
+ this.hoverStartTs = now;
4186
+ }
4187
+ }
4188
+ };
4189
+ function elementKey(el) {
4190
+ var _a2, _b, _c;
4191
+ return `${(_a2 = el.tag_name) != null ? _a2 : ""}|${(_b = el.attr__id) != null ? _b : ""}|${((_c = el.classes) != null ? _c : []).join(",")}`;
4192
+ }
4193
+
4194
+ // ../event-processor/dist/detectors/idle.js
4195
+ var IdleDetector = class {
4196
+ constructor(config, emit2) {
4197
+ this.config = config;
4198
+ this.emit = emit2;
4199
+ this.lastActivityTs = null;
4200
+ this.emitted = false;
4201
+ }
4202
+ ingest(raw) {
4203
+ if (raw.type !== 3)
4204
+ return;
4205
+ const src = raw.data.source;
4206
+ if (src === RRWebSource.MouseMove || src === RRWebSource.MouseInteraction || src === RRWebSource.Scroll) {
4207
+ this.lastActivityTs = raw.timestamp;
4208
+ this.emitted = false;
4209
+ }
4210
+ }
4211
+ tick(now) {
4212
+ if (this.lastActivityTs === null || this.emitted)
4213
+ return;
4214
+ if (now - this.lastActivityTs >= this.config.idleMs) {
4215
+ this.emit({
4216
+ ts: now,
4217
+ name: StandardEvents.UI_IDLE,
4218
+ source: "rrweb",
4219
+ schemaVersion: EVENT_SCHEMA_VERSION,
4220
+ props: {
4221
+ idle_ms: now - this.lastActivityTs
4222
+ }
4223
+ });
4224
+ this.emitted = true;
4225
+ }
4226
+ }
4227
+ };
4228
+
4229
+ // ../event-processor/dist/detectors/rage-click.js
4230
+ var RageClickDetector = class {
4231
+ constructor(config, emit2) {
4232
+ this.config = config;
4233
+ this.emit = emit2;
4234
+ this.clicks = [];
4235
+ }
4236
+ ingest(raw) {
4237
+ var _a2, _b;
4238
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseInteraction)
4239
+ return;
4240
+ if (raw.data.type !== RRWebMouseInteraction.Click)
4241
+ return;
4242
+ const x = (_a2 = raw.data.x) != null ? _a2 : 0;
4243
+ const y = (_b = raw.data.y) != null ? _b : 0;
4244
+ const ts = raw.timestamp;
4245
+ const cutoff = ts - this.config.rageClickWindowMs;
4246
+ this.clicks = this.clicks.filter((c) => c.ts >= cutoff);
4247
+ this.clicks.push({ ts, x, y });
4248
+ const nearby = this.clicks.filter((c) => {
4249
+ const dx = c.x - x;
4250
+ const dy = c.y - y;
4251
+ return Math.sqrt(dx * dx + dy * dy) <= this.config.rageClickRadiusPx;
4252
+ });
4253
+ if (nearby.length >= this.config.rageClickCount) {
4254
+ this.emit({
4255
+ ts,
4256
+ name: StandardEvents.UI_RAGE_CLICK,
4257
+ source: "rrweb",
4258
+ schemaVersion: EVENT_SCHEMA_VERSION,
4259
+ props: {
4260
+ x,
4261
+ y,
4262
+ clickCount: nearby.length,
4263
+ duration_ms: ts - nearby[0].ts
4264
+ }
4265
+ });
4266
+ this.clicks = [];
4267
+ }
4268
+ }
4269
+ };
4270
+
4271
+ // ../event-processor/dist/detectors/scroll-thrash.js
4272
+ var ScrollThrashDetector = class {
4273
+ constructor(config, emit2) {
4274
+ this.config = config;
4275
+ this.emit = emit2;
4276
+ this.lastY = null;
4277
+ this.lastDirection = null;
4278
+ this.reversals = [];
4279
+ }
4280
+ ingest(raw) {
4281
+ var _a2;
4282
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.Scroll)
4283
+ return;
4284
+ const y = (_a2 = raw.data.y) != null ? _a2 : 0;
4285
+ const ts = raw.timestamp;
4286
+ if (this.lastY !== null) {
4287
+ const direction = y > this.lastY ? "down" : y < this.lastY ? "up" : this.lastDirection;
4288
+ if (direction && direction !== this.lastDirection) {
4289
+ const cutoff = ts - this.config.scrollThrashWindowMs;
4290
+ this.reversals = this.reversals.filter((t) => t > cutoff);
4291
+ this.reversals.push(ts);
4292
+ if (this.reversals.length >= this.config.scrollThrashReversals) {
4293
+ this.emit({
4294
+ ts,
4295
+ name: StandardEvents.UI_SCROLL_THRASH,
4296
+ source: "rrweb",
4297
+ schemaVersion: EVENT_SCHEMA_VERSION,
4298
+ props: {
4299
+ reversals: this.reversals.length,
4300
+ duration_ms: ts - this.reversals[0]
4301
+ }
4302
+ });
4303
+ this.reversals = [];
4304
+ }
4305
+ }
4306
+ this.lastDirection = direction;
4307
+ }
4308
+ this.lastY = y;
4309
+ }
4310
+ };
4311
+
4312
+ // ../event-processor/dist/processor.js
4313
+ function createEventProcessor(options) {
4314
+ const config = { ...DEFAULT_DETECTOR_CONFIG, ...options == null ? void 0 : options.config };
4315
+ const listeners = [];
4316
+ function emit2(event) {
4317
+ for (const cb of listeners)
4318
+ cb(event);
4319
+ }
4320
+ const hesitation = new HesitationDetector(config, emit2);
4321
+ const rageClick = new RageClickDetector(config, emit2);
4322
+ const scrollThrash = new ScrollThrashDetector(config, emit2);
4323
+ const focusBounce = new FocusBounceDetector(config, emit2);
4324
+ const idle = new IdleDetector(config, emit2);
4325
+ const hover = new HoverTracker(config, emit2, options == null ? void 0 : options.elementResolver);
4326
+ const clickAttrBuffer = [];
4327
+ return {
4328
+ ingest(raw) {
4329
+ if (raw.kind === "posthog") {
4330
+ const { kind: _, ...phEvent } = raw;
4331
+ if (shouldNormalizeEvent(phEvent)) {
4332
+ emit2(normalizePostHogEvent(phEvent));
4333
+ }
4334
+ } else if (raw.kind === "rrweb") {
4335
+ hesitation.ingest(raw);
4336
+ rageClick.ingest(raw);
4337
+ scrollThrash.ingest(raw);
4338
+ focusBounce.ingest(raw);
4339
+ idle.ingest(raw);
4340
+ hover.ingest(raw);
4341
+ }
4342
+ },
4343
+ onEvent(callback) {
4344
+ listeners.push(callback);
4345
+ },
4346
+ tick(timestamp) {
4347
+ hesitation.tick(timestamp);
4348
+ idle.tick(timestamp);
4349
+ hover.tick(timestamp);
4350
+ },
4351
+ enrichClickAttributes(timestamp, elements) {
4352
+ clickAttrBuffer.push({ ts: timestamp, elements });
4353
+ const cutoff = timestamp - 500;
4354
+ while (clickAttrBuffer.length > 0 && clickAttrBuffer[0].ts < cutoff) {
4355
+ clickAttrBuffer.shift();
4356
+ }
4357
+ }
4358
+ };
4359
+ }
4360
+
4361
+ // src/events/types.ts
4362
+ var StandardEvents2 = {
3818
4363
  // UI events (from PostHog autocapture)
3819
4364
  UI_CLICK: "ui.click",
3820
4365
  UI_SCROLL: "ui.scroll",
@@ -3866,48 +4411,48 @@ function createCanvasEvent(name, props) {
3866
4411
  };
3867
4412
  }
3868
4413
  function canvasOpened(surface) {
3869
- return createCanvasEvent(StandardEvents.CANVAS_OPENED, { surface });
4414
+ return createCanvasEvent(StandardEvents2.CANVAS_OPENED, { surface });
3870
4415
  }
3871
4416
  function canvasClosed(surface) {
3872
- return createCanvasEvent(StandardEvents.CANVAS_CLOSED, { surface });
4417
+ return createCanvasEvent(StandardEvents2.CANVAS_CLOSED, { surface });
3873
4418
  }
3874
4419
  function tileViewed(tileId, surface) {
3875
- return createCanvasEvent(StandardEvents.TILE_VIEWED, { tileId, surface });
4420
+ return createCanvasEvent(StandardEvents2.TILE_VIEWED, { tileId, surface });
3876
4421
  }
3877
4422
  function tileExpanded(tileId, surface) {
3878
- return createCanvasEvent(StandardEvents.TILE_EXPANDED, { tileId, surface });
4423
+ return createCanvasEvent(StandardEvents2.TILE_EXPANDED, { tileId, surface });
3879
4424
  }
3880
4425
  function tileCollapsed(tileId, surface) {
3881
- return createCanvasEvent(StandardEvents.TILE_COLLAPSED, { tileId, surface });
4426
+ return createCanvasEvent(StandardEvents2.TILE_COLLAPSED, { tileId, surface });
3882
4427
  }
3883
4428
  function tileAction(tileId, actionId, surface) {
3884
- return createCanvasEvent(StandardEvents.TILE_ACTION, {
4429
+ return createCanvasEvent(StandardEvents2.TILE_ACTION, {
3885
4430
  tileId,
3886
4431
  actionId,
3887
4432
  surface
3888
4433
  });
3889
4434
  }
3890
4435
  function overlayStarted(recipeId, recipeName) {
3891
- return createCanvasEvent(StandardEvents.OVERLAY_STARTED, {
4436
+ return createCanvasEvent(StandardEvents2.OVERLAY_STARTED, {
3892
4437
  recipeId,
3893
4438
  recipeName
3894
4439
  });
3895
4440
  }
3896
4441
  function overlayCompleted(recipeId, recipeName) {
3897
- return createCanvasEvent(StandardEvents.OVERLAY_COMPLETED, {
4442
+ return createCanvasEvent(StandardEvents2.OVERLAY_COMPLETED, {
3898
4443
  recipeId,
3899
4444
  recipeName
3900
4445
  });
3901
4446
  }
3902
4447
  function overlayDismissed(recipeId, recipeName, stepIndex) {
3903
- return createCanvasEvent(StandardEvents.OVERLAY_DISMISSED, {
4448
+ return createCanvasEvent(StandardEvents2.OVERLAY_DISMISSED, {
3904
4449
  recipeId,
3905
4450
  recipeName,
3906
4451
  stepIndex
3907
4452
  });
3908
4453
  }
3909
4454
  function overlayStepViewed(recipeId, stepIndex, stepTitle) {
3910
- return createCanvasEvent(StandardEvents.OVERLAY_STEP_VIEWED, {
4455
+ return createCanvasEvent(StandardEvents2.OVERLAY_STEP_VIEWED, {
3911
4456
  recipeId,
3912
4457
  stepIndex,
3913
4458
  stepTitle
@@ -4233,7 +4778,7 @@ function useNotifications(eventBus, tiles) {
4233
4778
  const timerIds = useRef2(/* @__PURE__ */ new Map());
4234
4779
  const publishDismissed = useCallback2(
4235
4780
  (notif) => {
4236
- eventBus == null ? void 0 : eventBus.publish(StandardEvents.NOTIFICATION_DISMISSED, {
4781
+ eventBus == null ? void 0 : eventBus.publish(StandardEvents2.NOTIFICATION_DISMISSED, {
4237
4782
  notificationId: notif.id,
4238
4783
  tileId: notif.tileId,
4239
4784
  itemId: notif.itemId
@@ -4298,7 +4843,7 @@ function useNotifications(eventBus, tiles) {
4298
4843
  }
4299
4844
  return next;
4300
4845
  });
4301
- eventBus.publish(StandardEvents.NOTIFICATION_SHOWN, {
4846
+ eventBus.publish(StandardEvents2.NOTIFICATION_SHOWN, {
4302
4847
  notificationId: matched.id,
4303
4848
  tileId: matched.tileId,
4304
4849
  itemId: matched.itemId,
@@ -4511,7 +5056,7 @@ function WidgetMount({ widgetId, props }) {
4511
5056
  prevPropsJsonRef.current = propsJson;
4512
5057
  (_a3 = handleRef.current) == null ? void 0 : _a3.update(propsRef.current);
4513
5058
  }, [propsJson]);
4514
- if (!registry || !registry.has(widgetId)) {
5059
+ if (!(registry == null ? void 0 : registry.has(widgetId))) {
4515
5060
  return /* @__PURE__ */ jsxs2(
4516
5061
  "div",
4517
5062
  {
@@ -5132,7 +5677,7 @@ function ShadowCanvasOverlay({
5132
5677
  const handleNotificationClick = useCallback4(
5133
5678
  (notif) => {
5134
5679
  if (runtime3) {
5135
- runtime3.events.publish(StandardEvents.NOTIFICATION_CLICKED, {
5680
+ runtime3.events.publish(StandardEvents2.NOTIFICATION_CLICKED, {
5136
5681
  notificationId: notif.id,
5137
5682
  tileId: notif.tileId,
5138
5683
  itemId: notif.itemId
@@ -5142,7 +5687,7 @@ function ShadowCanvasOverlay({
5142
5687
  onToggle();
5143
5688
  }
5144
5689
  if (runtime3 && notif.tileId) {
5145
- runtime3.events.publish(StandardEvents.NOTIFICATION_DEEP_LINK, {
5690
+ runtime3.events.publish(StandardEvents2.NOTIFICATION_DEEP_LINK, {
5146
5691
  tileId: notif.tileId,
5147
5692
  itemId: notif.itemId
5148
5693
  });
@@ -5215,6 +5760,17 @@ function ShadowCanvasOverlay({
5215
5760
  }
5216
5761
  onToggle();
5217
5762
  }, [isOpen, telemetry, runtime3, onToggle]);
5763
+ useEffect7(() => {
5764
+ if (!isOpen) return;
5765
+ const handleOutsideClick = (e) => {
5766
+ const path = e.composedPath();
5767
+ if (containerRef.current && !path.includes(containerRef.current) && launcherRef.current && !path.includes(launcherRef.current)) {
5768
+ toggle2();
5769
+ }
5770
+ };
5771
+ document.addEventListener("mousedown", handleOutsideClick);
5772
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
5773
+ }, [isOpen, toggle2]);
5218
5774
  const onLauncherPointerDown = useCallback4((e) => {
5219
5775
  const rect = e.currentTarget.getBoundingClientRect();
5220
5776
  dragRef.current = {
@@ -5253,6 +5809,7 @@ function ShadowCanvasOverlay({
5253
5809
  const isPush = config.canvas.layout === "push";
5254
5810
  const canvasBorder = (_b = config.canvas.border) != null ? _b : "none";
5255
5811
  const containerRef = useRef5(null);
5812
+ const launcherRef = useRef5(null);
5256
5813
  const zIndex = 2147483600;
5257
5814
  useEffect7(() => {
5258
5815
  var _a3, _b2, _c2, _d2;
@@ -5340,7 +5897,7 @@ function ShadowCanvasOverlay({
5340
5897
  style: {
5341
5898
  position: "fixed",
5342
5899
  inset: 0,
5343
- pointerEvents: isOpen ? "auto" : "none",
5900
+ pointerEvents: "none",
5344
5901
  zIndex
5345
5902
  },
5346
5903
  children: /* @__PURE__ */ jsxs3("div", { style: wrapperStyle, children: [
@@ -5413,17 +5970,7 @@ function ShadowCanvasOverlay({
5413
5970
  ) }),
5414
5971
  footerSlot
5415
5972
  ] }),
5416
- /* @__PURE__ */ jsx8(
5417
- "div",
5418
- {
5419
- onClick: toggle2,
5420
- style: {
5421
- flex: "1 1 auto",
5422
- pointerEvents: isOpen ? "auto" : "none",
5423
- cursor: "default"
5424
- }
5425
- }
5426
- )
5973
+ /* @__PURE__ */ jsx8("div", { style: { flex: "1 1 auto" } })
5427
5974
  ] })
5428
5975
  }
5429
5976
  );
@@ -5453,6 +6000,7 @@ function ShadowCanvasOverlay({
5453
6000
  /* @__PURE__ */ jsxs3(
5454
6001
  "button",
5455
6002
  {
6003
+ ref: launcherRef,
5456
6004
  type: "button",
5457
6005
  "aria-label": "Toggle shadow canvas",
5458
6006
  className: launcherAnimate && !isOpen ? "syntro-launcher-animate" : void 0,
@@ -5715,8 +6263,8 @@ function SmartCanvasApp({
5715
6263
  controller,
5716
6264
  fetcher,
5717
6265
  configUri,
5718
- configUriFeatureKey = "smart-canvas-config-uri",
5719
- configFeatureKey = "smart-canvas-config",
6266
+ configUriFeatureKey,
6267
+ configFeatureKey,
5720
6268
  fetchCredentials = "include",
5721
6269
  pollIntervalMs,
5722
6270
  experiments,
@@ -5786,10 +6334,10 @@ function SmartCanvasAppInner({
5786
6334
  controller,
5787
6335
  fetcher,
5788
6336
  configUri,
5789
- configUriFeatureKey = "smart-canvas-config-uri",
5790
- configFeatureKey = "smart-canvas-config",
6337
+ configUriFeatureKey,
6338
+ configFeatureKey,
5791
6339
  fetchCredentials = "include",
5792
- pollIntervalMs,
6340
+ pollIntervalMs: _pollIntervalMs,
5793
6341
  experiments,
5794
6342
  telemetry,
5795
6343
  runtime: runtime3,
@@ -7072,7 +7620,6 @@ var PostHogAdapter = class {
7072
7620
  __publicField(this, "featureFlagsCallback");
7073
7621
  __publicField(this, "captureCallback");
7074
7622
  __publicField(this, "rrwebCallback");
7075
- __publicField(this, "consentUnsub");
7076
7623
  this.client = options.client;
7077
7624
  this.featureFlagsCallback = options.onFeatureFlagsLoaded;
7078
7625
  this.captureCallback = options.onCapture;
@@ -7083,7 +7630,7 @@ var PostHogAdapter = class {
7083
7630
  if (currentStatus === "granted") {
7084
7631
  this.initPostHog();
7085
7632
  }
7086
- this.consentUnsub = consent.subscribe((status) => {
7633
+ consent.subscribe((status) => {
7087
7634
  if (status === "granted") {
7088
7635
  if (!this.client) {
7089
7636
  this.initPostHog();
@@ -7110,71 +7657,67 @@ var PostHogAdapter = class {
7110
7657
  if (!options.apiKey) return;
7111
7658
  const enableFeatureFlags = (_a2 = options.enableFeatureFlags) != null ? _a2 : true;
7112
7659
  const instanceName = `syntro_${options.apiKey.slice(-6) || "sdk"}`;
7113
- this.client = posthog.init(
7114
- options.apiKey,
7115
- {
7116
- api_host: (_b = options.apiHost) != null ? _b : "https://telemetry.syntrologie.com",
7117
- // Feature flags for segment membership (in_segment_* flags)
7118
- // When enabled, /decide is called to get segment flags
7119
- advanced_disable_feature_flags: !enableFeatureFlags,
7120
- advanced_disable_feature_flags_on_first_load: !enableFeatureFlags,
7121
- // Full-page tracking - all ON by default
7122
- autocapture: (_c = options.autocapture) != null ? _c : true,
7123
- capture_pageview: (_d = options.capturePageview) != null ? _d : true,
7124
- capture_pageleave: (_e = options.capturePageleave) != null ? _e : true,
7125
- disable_session_recording: !((_f = options.sessionRecording) != null ? _f : true),
7126
- // CRITICAL: Disable user agent filtering to allow headless Chrome
7127
- // PostHog blocks "HeadlessChrome" user agents by default as bot detection
7128
- // This enables session recording in Playwright/crawler sessions
7129
- opt_out_useragent_filter: true,
7130
- // Cross-domain iframe recording for embeds
7131
- session_recording: {
7132
- recordCrossDomainIFrames: true
7133
- },
7134
- // Capture performance metrics
7135
- capture_performance: true,
7136
- // Enable web vitals
7137
- enable_recording_console_log: true,
7138
- // Bootstrap callback for when flags are loaded
7139
- loaded: (ph) => {
7140
- if (enableFeatureFlags && this.featureFlagsCallback) {
7141
- ph.onFeatureFlags(() => {
7142
- const allFlags = this.getAllFeatureFlags();
7143
- if (allFlags && this.featureFlagsCallback) {
7144
- this.featureFlagsCallback(allFlags);
7145
- }
7146
- });
7147
- const existingFlags = this.getAllFeatureFlags();
7148
- if (existingFlags && Object.keys(existingFlags).length > 0) {
7149
- this.featureFlagsCallback(existingFlags);
7660
+ const initOptions = {
7661
+ api_host: (_b = options.apiHost) != null ? _b : "https://telemetry.syntrologie.com",
7662
+ // Feature flags for segment membership (in_segment_* flags)
7663
+ // When enabled, /decide is called to get segment flags
7664
+ advanced_disable_feature_flags: !enableFeatureFlags,
7665
+ advanced_disable_feature_flags_on_first_load: !enableFeatureFlags,
7666
+ // Full-page tracking - all ON by default
7667
+ autocapture: (_c = options.autocapture) != null ? _c : true,
7668
+ capture_pageview: (_d = options.capturePageview) != null ? _d : "history_change",
7669
+ capture_pageleave: (_e = options.capturePageleave) != null ? _e : true,
7670
+ disable_session_recording: !((_f = options.sessionRecording) != null ? _f : true),
7671
+ // CRITICAL: Disable user agent filtering to allow headless Chrome
7672
+ // PostHog blocks "HeadlessChrome" user agents by default as bot detection
7673
+ // This enables session recording in Playwright/crawler sessions
7674
+ opt_out_useragent_filter: true,
7675
+ // Cross-domain iframe recording for embeds
7676
+ session_recording: {
7677
+ recordCrossDomainIFrames: true
7678
+ },
7679
+ // Capture performance metrics
7680
+ capture_performance: true,
7681
+ // Enable web vitals
7682
+ enable_recording_console_log: true,
7683
+ // Bootstrap callback for when flags are loaded
7684
+ loaded: (ph) => {
7685
+ if (enableFeatureFlags && this.featureFlagsCallback) {
7686
+ ph.onFeatureFlags(() => {
7687
+ const allFlags = this.getAllFeatureFlags();
7688
+ if (allFlags && this.featureFlagsCallback) {
7689
+ this.featureFlagsCallback(allFlags);
7150
7690
  }
7151
- }
7152
- if (this.captureCallback) {
7153
- ph.on("eventCaptured", (data) => {
7154
- var _a3;
7155
- const eventName = typeof data === "string" ? data : data == null ? void 0 : data.event;
7156
- const properties = typeof data === "string" ? void 0 : data == null ? void 0 : data.properties;
7157
- if (typeof eventName === "string") {
7158
- (_a3 = this.captureCallback) == null ? void 0 : _a3.call(this, eventName, properties);
7159
- }
7160
- });
7691
+ });
7692
+ const existingFlags = this.getAllFeatureFlags();
7693
+ if (existingFlags && Object.keys(existingFlags).length > 0) {
7694
+ this.featureFlagsCallback(existingFlags);
7161
7695
  }
7162
7696
  }
7163
- },
7697
+ if (this.captureCallback) {
7698
+ ph.on("eventCaptured", (...args) => {
7699
+ var _a3;
7700
+ const data = args[0];
7701
+ const eventName = typeof data === "string" ? data : data == null ? void 0 : data.event;
7702
+ const properties = typeof data === "string" ? void 0 : data == null ? void 0 : data.properties;
7703
+ if (typeof eventName === "string") {
7704
+ (_a3 = this.captureCallback) == null ? void 0 : _a3.call(this, eventName, properties);
7705
+ }
7706
+ });
7707
+ }
7708
+ }
7709
+ };
7710
+ const result = posthog.init(
7711
+ options.apiKey,
7712
+ initOptions,
7164
7713
  instanceName
7165
7714
  );
7715
+ if (result) {
7716
+ this.client = result;
7717
+ }
7166
7718
  if (this.rrwebCallback && this.client) {
7167
7719
  this.setupRRWebIntercept();
7168
7720
  }
7169
- if (options.replayMirrorEndpoint && this.client) {
7170
- import("./replayMirror-QZ3GQ527.js").then(({ setupReplayMirror }) => {
7171
- var _a3;
7172
- setupReplayMirror({
7173
- posthogHost: (_a3 = options.apiHost) != null ? _a3 : "https://telemetry.syntrologie.com",
7174
- mirrorEndpoint: options.replayMirrorEndpoint
7175
- });
7176
- });
7177
- }
7178
7721
  }
7179
7722
  /**
7180
7723
  * Set up rrweb event interception on PostHog's session recording.
@@ -7197,8 +7740,9 @@ var PostHogAdapter = class {
7197
7740
  return;
7198
7741
  }
7199
7742
  let recorder = null;
7200
- for (const key of Object.getOwnPropertyNames(sr)) {
7201
- const val = sr[key];
7743
+ const srRecord = sr;
7744
+ for (const key of Object.getOwnPropertyNames(srRecord)) {
7745
+ const val = srRecord[key];
7202
7746
  if (val && typeof val === "object" && typeof val.onRRwebEmit === "function" && typeof val.start === "function" && val !== sr) {
7203
7747
  recorder = val;
7204
7748
  break;
@@ -7225,9 +7769,8 @@ var PostHogAdapter = class {
7225
7769
  * Used to extract segment membership flags (in_segment_*).
7226
7770
  */
7227
7771
  getAllFeatureFlags() {
7228
- var _a2, _b, _c;
7229
- const flags = (_c = (_b = (_a2 = this.client) == null ? void 0 : _a2.featureFlags) == null ? void 0 : _b.getFlagVariants) == null ? void 0 : _c.call(_b);
7230
- return flags;
7772
+ var _a2, _b;
7773
+ return (_b = (_a2 = this.client) == null ? void 0 : _a2.featureFlags) == null ? void 0 : _b.getFlagVariants();
7231
7774
  }
7232
7775
  /**
7233
7776
  * Get segment membership flags (in_segment_*) from PostHog.
@@ -7578,187 +8121,36 @@ function hasExecutor(kind) {
7578
8121
  return executorRegistry.has(kind);
7579
8122
  }
7580
8123
 
7581
- // src/actions/validation.ts
7582
- var DANGEROUS_ATTRS = /* @__PURE__ */ new Set([
7583
- "onclick",
7584
- "onerror",
7585
- "onload",
7586
- "onmouseover",
7587
- "onfocus",
7588
- "onblur",
7589
- "onchange",
7590
- "onsubmit",
7591
- "onkeydown",
7592
- "onkeyup",
7593
- "onkeypress"
7594
- ]);
7595
- var MAX_HTML_LENGTH = 5e4;
7596
- var MAX_STYLE_COUNT = 50;
7597
- function validateAction(action) {
7598
- const errors = [];
7599
- const warnings = [];
7600
- if (!action || typeof action !== "object") {
8124
+ // src/actions/validation-rules.ts
8125
+ function validateBadgeAction(action, errors, warnings) {
8126
+ if (!action.content || typeof action.content !== "string") {
7601
8127
  errors.push({
7602
- code: "INVALID_ACTION",
7603
- message: "Action must be an object"
8128
+ code: "MISSING_CONTENT",
8129
+ message: "Badge action requires 'content' property",
8130
+ field: "content"
8131
+ });
8132
+ } else if (action.content.length > 100) {
8133
+ warnings.push({
8134
+ code: "LONG_BADGE_CONTENT",
8135
+ message: "Badge content is quite long",
8136
+ suggestion: "Keep badge content short (under 100 characters)"
7604
8137
  });
7605
- return { valid: false, errors, warnings };
7606
8138
  }
7607
- const { kind } = action;
7608
- if (!kind || typeof kind !== "string") {
8139
+ }
8140
+ function validateTooltipAction(action, errors, _warnings) {
8141
+ if (!action.content || typeof action.content !== "object") {
7609
8142
  errors.push({
7610
- code: "MISSING_KIND",
7611
- message: "Action must have a 'kind' property"
8143
+ code: "MISSING_CONTENT",
8144
+ message: "Tooltip action requires 'content' object",
8145
+ field: "content"
7612
8146
  });
7613
- return { valid: false, errors, warnings };
8147
+ return;
7614
8148
  }
7615
- if (!hasExecutor(kind) && kind !== "core:mountWidget") {
7616
- const registered = executorRegistry.list();
7617
- console.error(
7618
- `[ActionValidation] Unknown action kind: "${kind}". Registered kinds (${registered.length}): [${registered.join(", ")}]. This usually means the app that provides "${kind}" hasn't been activated yet, or the executor was never registered in ExecutorRegistry.`
7619
- );
8149
+ if (!action.content.body || typeof action.content.body !== "string") {
7620
8150
  errors.push({
7621
- code: "UNKNOWN_KIND",
7622
- message: `Unknown action kind: ${kind}`,
7623
- field: "kind"
7624
- });
7625
- return { valid: false, errors, warnings };
7626
- }
7627
- switch (kind) {
7628
- case "overlays:highlight":
7629
- case "overlays:pulse":
7630
- case "navigation:scrollTo":
7631
- validateAnchorAction(action, errors, warnings);
7632
- break;
7633
- case "overlays:badge":
7634
- validateAnchorAction(action, errors, warnings);
7635
- validateBadgeAction(action, errors, warnings);
7636
- break;
7637
- case "overlays:tooltip":
7638
- validateAnchorAction(action, errors, warnings);
7639
- validateTooltipAction(action, errors, warnings);
7640
- break;
7641
- case "overlays:modal":
7642
- validateModalAction(action, errors, warnings);
7643
- break;
7644
- case "content:insertHtml":
7645
- validateAnchorAction(action, errors, warnings);
7646
- validateInsertHtmlAction(action, errors, warnings);
7647
- break;
7648
- case "content:setText":
7649
- validateAnchorAction(action, errors, warnings);
7650
- validateSetTextAction(action, errors, warnings);
7651
- break;
7652
- case "content:setAttr":
7653
- validateAnchorAction(action, errors, warnings);
7654
- validateSetAttrAction(action, errors, warnings);
7655
- break;
7656
- case "content:addClass":
7657
- case "content:removeClass":
7658
- validateAnchorAction(action, errors, warnings);
7659
- validateClassAction(action, errors, warnings);
7660
- break;
7661
- case "content:setStyle":
7662
- validateAnchorAction(action, errors, warnings);
7663
- validateSetStyleAction(action, errors, warnings);
7664
- break;
7665
- case "core:mountWidget":
7666
- validateMountWidgetAction(action, errors, warnings);
7667
- break;
7668
- case "core:wait":
7669
- validateWaitAction(action, errors, warnings);
7670
- break;
7671
- case "core:sequence":
7672
- validateSequenceAction(action, errors, warnings);
7673
- break;
7674
- case "core:parallel":
7675
- validateParallelAction(action, errors, warnings);
7676
- break;
7677
- case "core:tour":
7678
- validateTourAction(action, errors, warnings);
7679
- break;
7680
- case "navigation:navigate":
7681
- validateNavigateAction(action, errors, warnings);
7682
- break;
7683
- }
7684
- return {
7685
- valid: errors.length === 0,
7686
- errors,
7687
- warnings
7688
- };
7689
- }
7690
- function validateAnchorAction(action, errors, warnings) {
7691
- const anchorId = action.anchorId;
7692
- if (!anchorId || typeof anchorId !== "object") {
7693
- errors.push({
7694
- code: "MISSING_ANCHOR_ID",
7695
- message: "Action requires an 'anchorId' object with a 'selector' string",
7696
- field: "anchorId"
7697
- });
7698
- return;
7699
- }
7700
- if (!anchorId.selector || typeof anchorId.selector !== "string") {
7701
- errors.push({
7702
- code: "MISSING_ANCHOR_SELECTOR",
7703
- message: "anchorId requires a 'selector' string",
7704
- field: "anchorId.selector"
7705
- });
7706
- } else if (anchorId.selector.length > 200) {
7707
- warnings.push({
7708
- code: "LONG_ANCHOR_ID",
7709
- message: "Anchor selector is unusually long",
7710
- suggestion: "Consider using a shorter, more descriptive selector"
7711
- });
7712
- }
7713
- if (anchorId.route === void 0 || anchorId.route === null) {
7714
- errors.push({
7715
- code: "MISSING_ANCHOR_ROUTE",
7716
- message: `anchorId requires a 'route' (string or array of strings). Use "**" for all routes.`,
7717
- field: "anchorId.route"
7718
- });
7719
- } else {
7720
- const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
7721
- for (const route of routes) {
7722
- if (typeof route !== "string") {
7723
- errors.push({
7724
- code: "INVALID_ANCHOR_ROUTE",
7725
- message: "anchorId.route must be a string or array of strings",
7726
- field: "anchorId.route"
7727
- });
7728
- break;
7729
- }
7730
- }
7731
- }
7732
- }
7733
- function validateBadgeAction(action, errors, warnings) {
7734
- if (!action.content || typeof action.content !== "string") {
7735
- errors.push({
7736
- code: "MISSING_CONTENT",
7737
- message: "Badge action requires 'content' property",
7738
- field: "content"
7739
- });
7740
- } else if (action.content.length > 100) {
7741
- warnings.push({
7742
- code: "LONG_BADGE_CONTENT",
7743
- message: "Badge content is quite long",
7744
- suggestion: "Keep badge content short (under 100 characters)"
7745
- });
7746
- }
7747
- }
7748
- function validateTooltipAction(action, errors, _warnings) {
7749
- if (!action.content || typeof action.content !== "object") {
7750
- errors.push({
7751
- code: "MISSING_CONTENT",
7752
- message: "Tooltip action requires 'content' object",
7753
- field: "content"
7754
- });
7755
- return;
7756
- }
7757
- if (!action.content.body || typeof action.content.body !== "string") {
7758
- errors.push({
7759
- code: "MISSING_BODY",
7760
- message: "Tooltip content requires 'body' property",
7761
- field: "content.body"
8151
+ code: "MISSING_BODY",
8152
+ message: "Tooltip content requires 'body' property",
8153
+ field: "content.body"
7762
8154
  });
7763
8155
  }
7764
8156
  }
@@ -8116,6 +8508,159 @@ function validateTourAction(action, errors, warnings) {
8116
8508
  }
8117
8509
  }
8118
8510
  }
8511
+
8512
+ // src/actions/validation-core.ts
8513
+ var DANGEROUS_ATTRS = /* @__PURE__ */ new Set([
8514
+ "onclick",
8515
+ "onerror",
8516
+ "onload",
8517
+ "onmouseover",
8518
+ "onfocus",
8519
+ "onblur",
8520
+ "onchange",
8521
+ "onsubmit",
8522
+ "onkeydown",
8523
+ "onkeyup",
8524
+ "onkeypress"
8525
+ ]);
8526
+ var MAX_HTML_LENGTH = 5e4;
8527
+ var MAX_STYLE_COUNT = 50;
8528
+ function validateAction(action) {
8529
+ const errors = [];
8530
+ const warnings = [];
8531
+ if (!action || typeof action !== "object") {
8532
+ errors.push({
8533
+ code: "INVALID_ACTION",
8534
+ message: "Action must be an object"
8535
+ });
8536
+ return { valid: false, errors, warnings };
8537
+ }
8538
+ const { kind } = action;
8539
+ if (!kind || typeof kind !== "string") {
8540
+ errors.push({
8541
+ code: "MISSING_KIND",
8542
+ message: "Action must have a 'kind' property"
8543
+ });
8544
+ return { valid: false, errors, warnings };
8545
+ }
8546
+ if (!hasExecutor(kind) && kind !== "core:mountWidget") {
8547
+ const registered = executorRegistry.list();
8548
+ console.error(
8549
+ `[ActionValidation] Unknown action kind: "${kind}". Registered kinds (${registered.length}): [${registered.join(", ")}]. This usually means the app that provides "${kind}" hasn't been activated yet, or the executor was never registered in ExecutorRegistry.`
8550
+ );
8551
+ errors.push({
8552
+ code: "UNKNOWN_KIND",
8553
+ message: `Unknown action kind: ${kind}`,
8554
+ field: "kind"
8555
+ });
8556
+ return { valid: false, errors, warnings };
8557
+ }
8558
+ switch (kind) {
8559
+ case "overlays:highlight":
8560
+ case "overlays:pulse":
8561
+ case "navigation:scrollTo":
8562
+ validateAnchorAction(action, errors, warnings);
8563
+ break;
8564
+ case "overlays:badge":
8565
+ validateAnchorAction(action, errors, warnings);
8566
+ validateBadgeAction(action, errors, warnings);
8567
+ break;
8568
+ case "overlays:tooltip":
8569
+ validateAnchorAction(action, errors, warnings);
8570
+ validateTooltipAction(action, errors, warnings);
8571
+ break;
8572
+ case "overlays:modal":
8573
+ validateModalAction(action, errors, warnings);
8574
+ break;
8575
+ case "content:insertHtml":
8576
+ validateAnchorAction(action, errors, warnings);
8577
+ validateInsertHtmlAction(action, errors, warnings);
8578
+ break;
8579
+ case "content:setText":
8580
+ validateAnchorAction(action, errors, warnings);
8581
+ validateSetTextAction(action, errors, warnings);
8582
+ break;
8583
+ case "content:setAttr":
8584
+ validateAnchorAction(action, errors, warnings);
8585
+ validateSetAttrAction(action, errors, warnings);
8586
+ break;
8587
+ case "content:addClass":
8588
+ case "content:removeClass":
8589
+ validateAnchorAction(action, errors, warnings);
8590
+ validateClassAction(action, errors, warnings);
8591
+ break;
8592
+ case "content:setStyle":
8593
+ validateAnchorAction(action, errors, warnings);
8594
+ validateSetStyleAction(action, errors, warnings);
8595
+ break;
8596
+ case "core:mountWidget":
8597
+ validateMountWidgetAction(action, errors, warnings);
8598
+ break;
8599
+ case "core:wait":
8600
+ validateWaitAction(action, errors, warnings);
8601
+ break;
8602
+ case "core:sequence":
8603
+ validateSequenceAction(action, errors, warnings);
8604
+ break;
8605
+ case "core:parallel":
8606
+ validateParallelAction(action, errors, warnings);
8607
+ break;
8608
+ case "overlays:tour":
8609
+ validateTourAction(action, errors, warnings);
8610
+ break;
8611
+ case "navigation:navigate":
8612
+ validateNavigateAction(action, errors, warnings);
8613
+ break;
8614
+ }
8615
+ return {
8616
+ valid: errors.length === 0,
8617
+ errors,
8618
+ warnings
8619
+ };
8620
+ }
8621
+ function validateAnchorAction(action, errors, warnings) {
8622
+ const anchorId = action.anchorId;
8623
+ if (!anchorId || typeof anchorId !== "object") {
8624
+ errors.push({
8625
+ code: "MISSING_ANCHOR_ID",
8626
+ message: "Action requires an 'anchorId' object with a 'selector' string",
8627
+ field: "anchorId"
8628
+ });
8629
+ return;
8630
+ }
8631
+ if (!anchorId.selector || typeof anchorId.selector !== "string") {
8632
+ errors.push({
8633
+ code: "MISSING_ANCHOR_SELECTOR",
8634
+ message: "anchorId requires a 'selector' string",
8635
+ field: "anchorId.selector"
8636
+ });
8637
+ } else if (anchorId.selector.length > 200) {
8638
+ warnings.push({
8639
+ code: "LONG_ANCHOR_ID",
8640
+ message: "Anchor selector is unusually long",
8641
+ suggestion: "Consider using a shorter, more descriptive selector"
8642
+ });
8643
+ }
8644
+ if (anchorId.route === void 0 || anchorId.route === null) {
8645
+ errors.push({
8646
+ code: "MISSING_ANCHOR_ROUTE",
8647
+ message: `anchorId requires a 'route' (string or array of strings). Use "**" for all routes.`,
8648
+ field: "anchorId.route"
8649
+ });
8650
+ } else {
8651
+ const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
8652
+ for (const route of routes) {
8653
+ if (typeof route !== "string") {
8654
+ errors.push({
8655
+ code: "INVALID_ANCHOR_ROUTE",
8656
+ message: "anchorId.route must be a string or array of strings",
8657
+ field: "anchorId.route"
8658
+ });
8659
+ break;
8660
+ }
8661
+ }
8662
+ }
8663
+ }
8119
8664
  function validateActions(actions) {
8120
8665
  var _a2;
8121
8666
  const errors = [];
@@ -8262,7 +8807,7 @@ function createActionEngine(options) {
8262
8807
  }
8263
8808
  return executor(action, context);
8264
8809
  }
8265
- function subscribeForReeval(id, action, triggerWhen, handle) {
8810
+ function subscribeForReeval(id, action, triggerWhen, _handle) {
8266
8811
  if (!runtime3) return;
8267
8812
  const unsubs = [];
8268
8813
  const onReeval = async () => {
@@ -9232,6 +9777,66 @@ function createEventAccumulator(options) {
9232
9777
  };
9233
9778
  }
9234
9779
 
9780
+ // src/events/validation.ts
9781
+ var APP_PREFIX = "app:";
9782
+ var RESERVED_PREFIX = "syntro:";
9783
+ var SEGMENT_PATTERN = /^[a-z][a-z0-9_]*$/;
9784
+ function validateEventName(name) {
9785
+ if (!name) {
9786
+ return { valid: false, reason: "Event name cannot be empty" };
9787
+ }
9788
+ if (name.startsWith(RESERVED_PREFIX)) {
9789
+ return { valid: false, reason: '"syntro:" prefix is reserved for internal SDK events' };
9790
+ }
9791
+ if (!name.startsWith(APP_PREFIX)) {
9792
+ return { valid: false, reason: `Custom events must start with "app:" prefix. Got: "${name}"` };
9793
+ }
9794
+ const segments = name.slice(APP_PREFIX.length).split(":");
9795
+ if (segments.length < 2) {
9796
+ return {
9797
+ valid: false,
9798
+ reason: `Event name must have at least 2 segments after "app:" (app:{category}:{action}). Got: "${name}"`
9799
+ };
9800
+ }
9801
+ for (const segment of segments) {
9802
+ if (!SEGMENT_PATTERN.test(segment)) {
9803
+ return {
9804
+ valid: false,
9805
+ reason: `Segment "${segment}" must be lowercase alphanumeric + underscores. Got: "${name}"`
9806
+ };
9807
+ }
9808
+ }
9809
+ return { valid: true };
9810
+ }
9811
+ function isSerializable(value) {
9812
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
9813
+ }
9814
+ function checkDepth(obj, maxDepth, current = 0) {
9815
+ if (current >= maxDepth) return false;
9816
+ for (const value of Object.values(obj)) {
9817
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
9818
+ if (!checkDepth(value, maxDepth, current + 1)) return false;
9819
+ }
9820
+ }
9821
+ return true;
9822
+ }
9823
+ function validateProps(props) {
9824
+ if (props === void 0) return { valid: true };
9825
+ if (props === null || typeof props !== "object" || Array.isArray(props)) {
9826
+ return { valid: false, reason: "Props must be a plain object" };
9827
+ }
9828
+ if (!checkDepth(props, 2)) {
9829
+ return { valid: false, reason: "Props nesting depth exceeds 2 levels" };
9830
+ }
9831
+ const stripped = [];
9832
+ for (const [key, value] of Object.entries(props)) {
9833
+ if (value !== null && typeof value === "object") continue;
9834
+ if (!isSerializable(value)) stripped.push(key);
9835
+ }
9836
+ if (stripped.length > 0) return { valid: true, stripped };
9837
+ return { valid: true };
9838
+ }
9839
+
9235
9840
  // src/events/EventBus.ts
9236
9841
  function matchesFilter(event, filter) {
9237
9842
  if (!filter) return true;
@@ -9263,8 +9868,16 @@ var EventBus = class {
9263
9868
  __publicField(this, "subscriptions", /* @__PURE__ */ new Set());
9264
9869
  __publicField(this, "history", []);
9265
9870
  __publicField(this, "maxHistorySize");
9266
- var _a2;
9871
+ __publicField(this, "debug");
9872
+ __publicField(this, "emitHistory");
9873
+ __publicField(this, "posthogCapture");
9874
+ __publicField(this, "testMode");
9875
+ var _a2, _b, _c, _d, _e;
9267
9876
  this.maxHistorySize = (_a2 = options.maxHistorySize) != null ? _a2 : 100;
9877
+ this.debug = (_b = options.debug) != null ? _b : false;
9878
+ this.emitHistory = (_c = options.history) != null ? _c : null;
9879
+ this.posthogCapture = (_d = options.posthogCapture) != null ? _d : null;
9880
+ this.testMode = (_e = options.testMode) != null ? _e : false;
9268
9881
  }
9269
9882
  /**
9270
9883
  * Subscribe to events matching an optional filter.
@@ -9287,35 +9900,112 @@ var EventBus = class {
9287
9900
  };
9288
9901
  }
9289
9902
  /**
9290
- * Publish an event to all matching subscribers.
9903
+ * Publish an event to all matching subscribers.
9904
+ */
9905
+ publish(name, props, source = "canvas") {
9906
+ const event = {
9907
+ ts: Date.now(),
9908
+ name,
9909
+ source,
9910
+ props,
9911
+ schemaVersion: EVENT_SCHEMA_VERSION
9912
+ };
9913
+ this.publishEvent(event);
9914
+ }
9915
+ /**
9916
+ * Publish a pre-constructed NormalizedEvent.
9917
+ */
9918
+ publishEvent(event) {
9919
+ this.history.push(event);
9920
+ if (this.history.length > this.maxHistorySize) {
9921
+ this.history.shift();
9922
+ }
9923
+ for (const subscription of this.subscriptions) {
9924
+ if (matchesFilter(event, subscription.filter)) {
9925
+ try {
9926
+ subscription.callback(event);
9927
+ } catch (err) {
9928
+ console.error("[EventBus] Subscriber error:", err);
9929
+ }
9930
+ }
9931
+ }
9932
+ }
9933
+ /**
9934
+ * Emit a validated custom event from the host application.
9935
+ *
9936
+ * Custom events must use the `app:` prefix (e.g. `app:cart:abandoned`).
9937
+ * In debug mode, returns an EmitResult with delivery details.
9938
+ * In production mode, returns undefined.
9291
9939
  */
9292
- publish(name, props, source = "canvas") {
9940
+ emit(name, props) {
9941
+ const nameResult = validateEventName(name);
9942
+ if (!nameResult.valid) {
9943
+ console.warn(`[EventBus] emit() rejected: ${nameResult.reason}`);
9944
+ return this.debug ? { delivered: false, matchedRules: [], posthogCaptured: false, listenersNotified: 0 } : void 0;
9945
+ }
9946
+ const propsResult = validateProps(props);
9947
+ if (!propsResult.valid) {
9948
+ console.warn(`[EventBus] emit() rejected props: ${propsResult.reason}`);
9949
+ return this.debug ? { delivered: false, matchedRules: [], posthogCaptured: false, listenersNotified: 0 } : void 0;
9950
+ }
9293
9951
  const event = {
9294
9952
  ts: Date.now(),
9295
9953
  name,
9296
- source,
9954
+ source: "custom",
9297
9955
  props,
9298
9956
  schemaVersion: EVENT_SCHEMA_VERSION
9299
9957
  };
9300
- this.publishEvent(event);
9301
- }
9302
- /**
9303
- * Publish a pre-constructed NormalizedEvent.
9304
- */
9305
- publishEvent(event) {
9306
9958
  this.history.push(event);
9307
9959
  if (this.history.length > this.maxHistorySize) {
9308
9960
  this.history.shift();
9309
9961
  }
9962
+ let listenersNotified = 0;
9310
9963
  for (const subscription of this.subscriptions) {
9311
9964
  if (matchesFilter(event, subscription.filter)) {
9312
9965
  try {
9313
9966
  subscription.callback(event);
9967
+ listenersNotified++;
9314
9968
  } catch (err) {
9315
9969
  console.error("[EventBus] Subscriber error:", err);
9970
+ listenersNotified++;
9316
9971
  }
9317
9972
  }
9318
9973
  }
9974
+ let posthogCaptured = false;
9975
+ if (this.posthogCapture && !this.testMode) {
9976
+ try {
9977
+ this.posthogCapture(name, props);
9978
+ posthogCaptured = true;
9979
+ } catch (err) {
9980
+ console.error("[EventBus] PostHog capture error:", err);
9981
+ }
9982
+ }
9983
+ if (this.emitHistory) {
9984
+ this.emitHistory.record({
9985
+ name,
9986
+ props,
9987
+ source: "custom",
9988
+ timestamp: event.ts,
9989
+ matchedRules: []
9990
+ });
9991
+ }
9992
+ if (this.debug) {
9993
+ console.debug("[EventBus] emit()", { name, props, listenersNotified, posthogCaptured });
9994
+ return {
9995
+ delivered: true,
9996
+ matchedRules: [],
9997
+ posthogCaptured,
9998
+ listenersNotified
9999
+ };
10000
+ }
10001
+ return void 0;
10002
+ }
10003
+ /**
10004
+ * Set the PostHog capture function after construction.
10005
+ * Used by bootstrap to wire PostHog after the EventBus is created.
10006
+ */
10007
+ setPosthogCapture(fn) {
10008
+ this.posthogCapture = fn;
9319
10009
  }
9320
10010
  /**
9321
10011
  * Get recent events matching an optional filter.
@@ -9367,6 +10057,27 @@ function createEventBus(options = {}) {
9367
10057
  return new EventBus(options);
9368
10058
  }
9369
10059
 
10060
+ // src/events/history.ts
10061
+ var EventHistory = class {
10062
+ constructor(maxSize = 100) {
10063
+ __publicField(this, "entries", []);
10064
+ __publicField(this, "maxSize");
10065
+ this.maxSize = maxSize;
10066
+ }
10067
+ record(entry) {
10068
+ this.entries.push(entry);
10069
+ if (this.maxSize > 0 && this.entries.length > this.maxSize) {
10070
+ this.entries.shift();
10071
+ }
10072
+ }
10073
+ getAll() {
10074
+ return [...this.entries];
10075
+ }
10076
+ clear() {
10077
+ this.entries = [];
10078
+ }
10079
+ };
10080
+
9370
10081
  // src/navigation/NavigationMonitor.ts
9371
10082
  var NavigationMonitor = class {
9372
10083
  constructor() {
@@ -10478,7 +11189,16 @@ function matchesAnchorRoute(anchorId) {
10478
11189
  const pathname = typeof window !== "undefined" ? window.location.pathname : "/";
10479
11190
  const normalizedPath = pathname.replace(/\/$/, "") || "/";
10480
11191
  const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
10481
- return routes.some((pattern) => matchRoutePattern(normalizedPath, pattern));
11192
+ return routes.some((pattern) => {
11193
+ let normalized = pattern;
11194
+ if (/^https?:\/\//.test(normalized)) {
11195
+ try {
11196
+ normalized = new URL(normalized).pathname;
11197
+ } catch {
11198
+ }
11199
+ }
11200
+ return matchRoutePattern(normalizedPath, normalized);
11201
+ });
10482
11202
  }
10483
11203
  function createSmartCanvasRuntime(options = {}) {
10484
11204
  var _a2, _b, _c, _d;
@@ -10650,8 +11370,141 @@ function encodeToken(payload) {
10650
11370
  return TOKEN_PREFIX + base64;
10651
11371
  }
10652
11372
 
10653
- // src/bootstrap.ts
10654
- import { createEventProcessor } from "@syntrologie/event-processor";
11373
+ // src/bootstrap-init.ts
11374
+ function getEnvVar(name) {
11375
+ if (typeof process !== "undefined" && process.env) {
11376
+ return process.env[name];
11377
+ }
11378
+ try {
11379
+ const meta = (0, eval)("import.meta");
11380
+ if (meta == null ? void 0 : meta.env) {
11381
+ return meta.env[name];
11382
+ }
11383
+ } catch {
11384
+ }
11385
+ return void 0;
11386
+ }
11387
+ var SEGMENT_CACHE_KEY = "syntro_segment_attributes";
11388
+ function loadCachedSegmentAttributes() {
11389
+ if (typeof window === "undefined") return {};
11390
+ try {
11391
+ const cached = localStorage.getItem(SEGMENT_CACHE_KEY);
11392
+ if (cached) {
11393
+ const attrs = JSON.parse(cached);
11394
+ debug("Syntro Bootstrap", "Loaded cached segment attributes:", attrs);
11395
+ return attrs;
11396
+ }
11397
+ } catch (err) {
11398
+ warn("Syntro Bootstrap", "Failed to load cached segment attributes:", err);
11399
+ }
11400
+ return {};
11401
+ }
11402
+ function cacheSegmentAttributes(attrs) {
11403
+ if (typeof window === "undefined") return;
11404
+ try {
11405
+ localStorage.setItem(SEGMENT_CACHE_KEY, JSON.stringify(attrs));
11406
+ debug("Syntro Bootstrap", "Cached segment attributes:", attrs);
11407
+ } catch (err) {
11408
+ warn("Syntro Bootstrap", "Failed to cache segment attributes:", err);
11409
+ }
11410
+ }
11411
+ function extractSegmentFlags(allFlags) {
11412
+ if (!allFlags) return {};
11413
+ const segmentFlags = {};
11414
+ for (const [key, value] of Object.entries(allFlags)) {
11415
+ if (key.startsWith("in_segment_")) {
11416
+ segmentFlags[key] = value === true;
11417
+ }
11418
+ }
11419
+ return segmentFlags;
11420
+ }
11421
+ function collectBrowserMetadata() {
11422
+ var _a2;
11423
+ if (typeof window === "undefined") return {};
11424
+ const attrs = {};
11425
+ try {
11426
+ const params = new URLSearchParams(window.location.search);
11427
+ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
11428
+ const val = params.get(key);
11429
+ if (val) attrs[key] = val;
11430
+ }
11431
+ } catch {
11432
+ }
11433
+ try {
11434
+ if (navigator.language) attrs.browser_language = navigator.language;
11435
+ if ((_a2 = navigator.languages) == null ? void 0 : _a2.length) attrs.browser_languages = [...navigator.languages];
11436
+ } catch {
11437
+ }
11438
+ try {
11439
+ const w = window.innerWidth;
11440
+ attrs.device_type = w < 768 ? "mobile" : w < 1024 ? "tablet" : "desktop";
11441
+ } catch {
11442
+ }
11443
+ try {
11444
+ if (document.referrer) {
11445
+ attrs.referrer = document.referrer;
11446
+ try {
11447
+ attrs.referrer_hostname = new URL(document.referrer).hostname;
11448
+ } catch {
11449
+ }
11450
+ }
11451
+ } catch {
11452
+ }
11453
+ try {
11454
+ const ua = navigator.userAgent;
11455
+ if (ua.includes("Edg/")) attrs.browser = "Edge";
11456
+ else if (ua.includes("OPR/") || ua.includes("Opera")) attrs.browser = "Opera";
11457
+ else if (ua.includes("Chrome/") && !ua.includes("Chromium")) attrs.browser = "Chrome";
11458
+ else if (ua.includes("Safari/") && !ua.includes("Chrome")) attrs.browser = "Safari";
11459
+ else if (ua.includes("Firefox/")) attrs.browser = "Firefox";
11460
+ if (ua.includes("Windows")) attrs.os = "Windows";
11461
+ else if (ua.includes("iPhone") || ua.includes("iPad")) attrs.os = "iOS";
11462
+ else if (ua.includes("Mac OS X") || ua.includes("Macintosh")) attrs.os = "macOS";
11463
+ else if (ua.includes("Android")) attrs.os = "Android";
11464
+ else if (ua.includes("Linux")) attrs.os = "Linux";
11465
+ } catch {
11466
+ }
11467
+ try {
11468
+ attrs.page_url = window.location.href;
11469
+ attrs.page_path = window.location.pathname;
11470
+ attrs.page_host = window.location.hostname;
11471
+ if (window.location.search) attrs.page_query = window.location.search;
11472
+ } catch {
11473
+ }
11474
+ return attrs;
11475
+ }
11476
+ var GEO_CACHE_KEY = "syntro_geo";
11477
+ var GEO_DEFAULT_HOST = "https://geo.syntrologie.com";
11478
+ async function fetchGeo(geoHost) {
11479
+ if (typeof window === "undefined") return {};
11480
+ try {
11481
+ const cached = localStorage.getItem(GEO_CACHE_KEY);
11482
+ if (cached) {
11483
+ const parsed = JSON.parse(cached);
11484
+ debug("Syntro Bootstrap", "Geo: using cached data:", parsed);
11485
+ return parsed;
11486
+ }
11487
+ } catch {
11488
+ }
11489
+ try {
11490
+ const res = await fetch(geoHost, { signal: AbortSignal.timeout(2e3) });
11491
+ if (res.ok) {
11492
+ const geo = await res.json();
11493
+ const cleaned = {};
11494
+ for (const [k, v] of Object.entries(geo)) {
11495
+ if (typeof v === "string" && v) cleaned[k] = v;
11496
+ }
11497
+ try {
11498
+ localStorage.setItem(GEO_CACHE_KEY, JSON.stringify(cleaned));
11499
+ } catch {
11500
+ }
11501
+ debug("Syntro Bootstrap", "Geo: fetched from worker:", cleaned);
11502
+ return cleaned;
11503
+ }
11504
+ } catch {
11505
+ }
11506
+ return {};
11507
+ }
10655
11508
 
10656
11509
  // src/experiments/registry.ts
10657
11510
  var adapters = {
@@ -10790,6 +11643,11 @@ var ExperimentsFetcher = class {
10790
11643
  };
10791
11644
  }
10792
11645
  }
11646
+ if (!this.featureKey) {
11647
+ throw new Error(
11648
+ "[SmartCanvas] No featureKey configured and no variant flags found. Ensure at least one config feature flag exists in your experiment platform."
11649
+ );
11650
+ }
10793
11651
  const config = (_b = (_a2 = this.client).getFeatureValue) == null ? void 0 : _b.call(_a2, this.featureKey, null);
10794
11652
  if (!config || typeof config !== "object") {
10795
11653
  throw new Error(
@@ -10901,95 +11759,9 @@ function createTelemetryClient(provider, config) {
10901
11759
  return factory(config);
10902
11760
  }
10903
11761
 
10904
- // src/bootstrap.ts
10905
- function getEnvVar(name) {
10906
- if (typeof process !== "undefined" && process.env) {
10907
- return process.env[name];
10908
- }
10909
- try {
10910
- const meta = (0, eval)("import.meta");
10911
- if (meta == null ? void 0 : meta.env) {
10912
- return meta.env[name];
10913
- }
10914
- } catch {
10915
- }
10916
- return void 0;
10917
- }
10918
- var SEGMENT_CACHE_KEY = "syntro_segment_attributes";
10919
- function loadCachedSegmentAttributes() {
10920
- if (typeof window === "undefined") return {};
10921
- try {
10922
- const cached = localStorage.getItem(SEGMENT_CACHE_KEY);
10923
- if (cached) {
10924
- const attrs = JSON.parse(cached);
10925
- debug("Syntro Bootstrap", "Loaded cached segment attributes:", attrs);
10926
- return attrs;
10927
- }
10928
- } catch (err) {
10929
- warn("Syntro Bootstrap", "Failed to load cached segment attributes:", err);
10930
- }
10931
- return {};
10932
- }
10933
- function cacheSegmentAttributes(attrs) {
10934
- if (typeof window === "undefined") return;
10935
- try {
10936
- localStorage.setItem(SEGMENT_CACHE_KEY, JSON.stringify(attrs));
10937
- debug("Syntro Bootstrap", "Cached segment attributes:", attrs);
10938
- } catch (err) {
10939
- warn("Syntro Bootstrap", "Failed to cache segment attributes:", err);
10940
- }
10941
- }
10942
- function extractSegmentFlags(allFlags) {
10943
- if (!allFlags) return {};
10944
- const segmentFlags = {};
10945
- for (const [key, value] of Object.entries(allFlags)) {
10946
- if (key.startsWith("in_segment_")) {
10947
- segmentFlags[key] = value === true;
10948
- }
10949
- }
10950
- return segmentFlags;
10951
- }
10952
- function collectBrowserMetadata() {
10953
- var _a2;
10954
- if (typeof window === "undefined") return {};
10955
- const attrs = {};
10956
- try {
10957
- const params = new URLSearchParams(window.location.search);
10958
- for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
10959
- const val = params.get(key);
10960
- if (val) attrs[key] = val;
10961
- }
10962
- } catch {
10963
- }
10964
- try {
10965
- if (navigator.language) attrs.browser_language = navigator.language;
10966
- if ((_a2 = navigator.languages) == null ? void 0 : _a2.length) attrs.browser_languages = [...navigator.languages];
10967
- } catch {
10968
- }
10969
- try {
10970
- const w = window.innerWidth;
10971
- attrs.device_type = w < 768 ? "mobile" : w < 1024 ? "tablet" : "desktop";
10972
- } catch {
10973
- }
10974
- try {
10975
- if (document.referrer) {
10976
- attrs.referrer = document.referrer;
10977
- try {
10978
- attrs.referrer_hostname = new URL(document.referrer).hostname;
10979
- } catch {
10980
- }
10981
- }
10982
- } catch {
10983
- }
10984
- try {
10985
- attrs.page_url = window.location.href;
10986
- attrs.page_path = window.location.pathname;
10987
- } catch {
10988
- }
10989
- return attrs;
10990
- }
10991
- async function init(options) {
10992
- var _a2, _b, _c, _d, _e, _f;
11762
+ // src/bootstrap-runtime.ts
11763
+ async function _initCore(options) {
11764
+ var _a2, _b, _c, _d, _e, _f, _g;
10993
11765
  initLogger();
10994
11766
  debug("Syntro Bootstrap", "====== INIT ======");
10995
11767
  debug("Syntro Bootstrap", "Options:", {
@@ -11054,13 +11826,20 @@ async function init(options) {
11054
11826
  const experimentHost = getEnvVar("NEXT_PUBLIC_SYNTRO_EXPERIMENT_HOST") || getEnvVar("VITE_SYNTRO_EXPERIMENT_HOST") || (payload == null ? void 0 : payload.eh);
11055
11827
  const telemetryHost = getEnvVar("NEXT_PUBLIC_SYNTRO_TELEMETRY_HOST") || getEnvVar("VITE_SYNTRO_TELEMETRY_HOST") || (payload == null ? void 0 : payload.th);
11056
11828
  const editorUrl = getEnvVar("NEXT_PUBLIC_SYNTRO_EDITOR_URL") || getEnvVar("VITE_SYNTRO_EDITOR_URL") || ((_b = options.canvas) == null ? void 0 : _b.editorUrl);
11829
+ const geoHost = (payload == null ? void 0 : payload.g) || getEnvVar("NEXT_PUBLIC_SYNTRO_GEO_HOST") || getEnvVar("VITE_SYNTRO_GEO_HOST") || GEO_DEFAULT_HOST;
11057
11830
  const cachedSegmentAttrs = loadCachedSegmentAttributes();
11058
11831
  const browserMetadata = collectBrowserMetadata();
11059
11832
  const phaseOneAttrs = { ...browserMetadata, ...cachedSegmentAttrs };
11060
11833
  debug("Syntro Bootstrap", "Phase 1: Browser metadata:", browserMetadata);
11061
11834
  debug("Syntro Bootstrap", "Phase 1: Cached segment attributes:", cachedSegmentAttrs);
11835
+ const geoPromise = fetchGeo(geoHost);
11062
11836
  let experiments;
11063
- const events = createEventBus();
11837
+ const isDebugOrTest = options.debug || options.testMode;
11838
+ const events = createEventBus({
11839
+ debug: options.debug,
11840
+ history: isDebugOrTest ? new EventHistory(options.testMode ? 0 : 100) : void 0,
11841
+ testMode: options.testMode
11842
+ });
11064
11843
  console.log("[Syntro Bootstrap] EventBus created");
11065
11844
  const processor = createEventProcessor({
11066
11845
  elementResolver: typeof window !== "undefined" ? (x, y) => {
@@ -11122,6 +11901,9 @@ async function init(options) {
11122
11901
  // undefined falls back to adapter default
11123
11902
  // Enable PostHog feature flags for segment membership
11124
11903
  enableFeatureFlags: true,
11904
+ // Disable session recording in debug/dev mode (mock telemetry doesn't
11905
+ // support the PostHog recorder extension, causing console errors)
11906
+ sessionRecording: !payload.d,
11125
11907
  // Wire up callback for when flags are loaded (Phase 2)
11126
11908
  onFeatureFlagsLoaded,
11127
11909
  // Wire up event capture to feed into event processor
@@ -11134,6 +11916,11 @@ async function init(options) {
11134
11916
  }
11135
11917
  });
11136
11918
  console.log(`[Syntro Bootstrap] Telemetry client created (${provider}) with EventBus wiring`);
11919
+ const telemetryForCapture = telemetry;
11920
+ events.setPosthogCapture((name, props) => {
11921
+ var _a3;
11922
+ (_a3 = telemetryForCapture.track) == null ? void 0 : _a3.call(telemetryForCapture, name, props);
11923
+ });
11137
11924
  }
11138
11925
  let sessionMetrics;
11139
11926
  if (payload == null ? void 0 : payload.e) {
@@ -11226,11 +12013,17 @@ async function init(options) {
11226
12013
  warn("Syntro Bootstrap", "Failed to load GrowthBook features:", err);
11227
12014
  }
11228
12015
  }
12016
+ const geoData = await geoPromise;
12017
+ if (experiments && Object.keys(geoData).length > 0) {
12018
+ const mergedAttrs = { ...browserMetadata, ...geoData };
12019
+ debug("Syntro Bootstrap", "Merging geo data into GrowthBook attributes:", geoData);
12020
+ (_f = experiments.setAttributes) == null ? void 0 : _f.call(experiments, mergedAttrs);
12021
+ }
11229
12022
  let baseFetcher;
11230
12023
  if (options.fetcher) {
11231
12024
  baseFetcher = options.fetcher;
11232
12025
  } else if (payload == null ? void 0 : payload.f) {
11233
- const configFetcher = createConfigFetcher(payload.f, (_f = payload.o) != null ? _f : {});
12026
+ const configFetcher = createConfigFetcher(payload.f, (_g = payload.o) != null ? _g : {});
11234
12027
  baseFetcher = async () => {
11235
12028
  var _a3;
11236
12029
  const result = await configFetcher.fetch();
@@ -11244,7 +12037,7 @@ async function init(options) {
11244
12037
  }
11245
12038
  const warnedAppFailures = /* @__PURE__ */ new Set();
11246
12039
  const appLoadingFetcher = baseFetcher ? async () => {
11247
- var _a3, _b2, _c2, _d2, _e2, _f2, _g;
12040
+ var _a3, _b2, _c2, _d2, _e2, _f2, _g2;
11248
12041
  const config = await baseFetcher();
11249
12042
  console.log(
11250
12043
  "[Syntro Bootstrap] Config fetched:",
@@ -11252,7 +12045,7 @@ async function init(options) {
11252
12045
  `actions=${(_d2 = (_c2 = config.actions) == null ? void 0 : _c2.length) != null ? _d2 : 0},`,
11253
12046
  `theme=${(_f2 = (_e2 = config.theme) == null ? void 0 : _e2.name) != null ? _f2 : "none"}`
11254
12047
  );
11255
- if (((_g = config.actions) == null ? void 0 : _g.length) > 0) {
12048
+ if (((_g2 = config.actions) == null ? void 0 : _g2.length) > 0) {
11256
12049
  console.log(
11257
12050
  "[Syntro Bootstrap] Actions in config:",
11258
12051
  config.actions.map(
@@ -11315,10 +12108,33 @@ async function init(options) {
11315
12108
  });
11316
12109
  return { canvas, runtime: runtime3, experiments, telemetry, sessionMetrics, appLoader };
11317
12110
  }
12111
+
12112
+ // src/bootstrap.ts
12113
+ async function init(options) {
12114
+ var _a2;
12115
+ try {
12116
+ return await _initCore(options);
12117
+ } catch (err) {
12118
+ const message = err instanceof Error ? err.message : String(err);
12119
+ console.warn("[Syntrologie] SDK initialization failed:", message);
12120
+ if (typeof document !== "undefined") {
12121
+ (_a2 = document.getElementById("syntrologie-anti-flicker")) == null ? void 0 : _a2.remove();
12122
+ }
12123
+ return void 0;
12124
+ }
12125
+ }
12126
+ function emit(eventName, props = {}) {
12127
+ var _a2, _b;
12128
+ if (typeof window === "undefined") return;
12129
+ const runtime3 = (_a2 = window.SynOS) == null ? void 0 : _a2.runtime;
12130
+ if (!((_b = runtime3 == null ? void 0 : runtime3.events) == null ? void 0 : _b.publish)) return;
12131
+ runtime3.events.publish({ name: eventName, source: "custom", props });
12132
+ }
11318
12133
  var Syntro = {
11319
12134
  init,
11320
12135
  encodeToken,
11321
- decodeToken
12136
+ decodeToken,
12137
+ events: { emit }
11322
12138
  };
11323
12139
  if (typeof window !== "undefined") {
11324
12140
  window.Syntro = Syntro;
@@ -11349,8 +12165,11 @@ export {
11349
12165
  createSmartCanvasController,
11350
12166
  ShadowRootProvider,
11351
12167
  useShadowRoot,
11352
- StandardEvents,
11353
12168
  EVENT_SCHEMA_VERSION,
12169
+ normalizePostHogEvent,
12170
+ shouldNormalizeEvent,
12171
+ createPostHogNormalizer,
12172
+ StandardEvents2 as StandardEvents,
11354
12173
  CanvasEvents,
11355
12174
  NotificationToastStack,
11356
12175
  MAX_VISIBLE_TOASTS,
@@ -11399,8 +12218,11 @@ export {
11399
12218
  evaluateSync,
11400
12219
  createDecisionEngine,
11401
12220
  createEventAccumulator,
12221
+ validateEventName,
12222
+ validateProps,
11402
12223
  EventBus,
11403
12224
  createEventBus,
12225
+ EventHistory,
11404
12226
  NavigationMonitor,
11405
12227
  StateStore,
11406
12228
  createStateStore,
@@ -11424,4 +12246,4 @@ export {
11424
12246
  encodeToken,
11425
12247
  Syntro
11426
12248
  };
11427
- //# sourceMappingURL=chunk-H3FAYTUV.js.map
12249
+ //# sourceMappingURL=chunk-J2LGX2PV.js.map