@syntrologie/runtime-sdk 2.12.0 → 2.14.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 (37) hide show
  1. package/CAPABILITIES.md +96 -67
  2. package/dist/actions/schema.d.ts +776 -776
  3. package/dist/actions/schema.js +3 -3
  4. package/dist/bootstrap.d.ts +2 -0
  5. package/dist/{chunk-L6RJMBR2.js → chunk-77TNZ66J.js} +3 -3
  6. package/dist/{chunk-XDYJ64IN.js → chunk-IR6UOR63.js} +4 -4
  7. package/dist/chunk-IR6UOR63.js.map +7 -0
  8. package/dist/{chunk-J2LGX2PV.js → chunk-JCDCANR7.js} +296 -75
  9. package/dist/chunk-JCDCANR7.js.map +7 -0
  10. package/dist/chunk-YLLWLUQX.js +241 -0
  11. package/dist/chunk-YLLWLUQX.js.map +7 -0
  12. package/dist/components/ShadowCanvasOverlay.d.ts +1 -2
  13. package/dist/config/schema.d.ts +147 -136
  14. package/dist/config/schema.js +2 -2
  15. package/dist/decisions/schema.d.ts +47 -47
  16. package/dist/decisions/schema.js +1 -1
  17. package/dist/fetchers/experimentsFetcher.d.ts +3 -3
  18. package/dist/fetchers/mergeConfigs.d.ts +7 -7
  19. package/dist/index.js +6 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.js +4 -4
  22. package/dist/smart-canvas.esm.js +44 -44
  23. package/dist/smart-canvas.esm.js.map +4 -4
  24. package/dist/smart-canvas.js +341 -109
  25. package/dist/smart-canvas.js.map +4 -4
  26. package/dist/smart-canvas.min.js +43 -43
  27. package/dist/smart-canvas.min.js.map +4 -4
  28. package/dist/telemetry/InterventionTracker.d.ts +23 -0
  29. package/dist/telemetry/index.d.ts +1 -0
  30. package/dist/version.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/schema/canvas-config.schema.json +2347 -11396
  33. package/dist/chunk-BU4Z6PD7.js +0 -218
  34. package/dist/chunk-BU4Z6PD7.js.map +0 -7
  35. package/dist/chunk-J2LGX2PV.js.map +0 -7
  36. package/dist/chunk-XDYJ64IN.js.map +0 -7
  37. /package/dist/{chunk-L6RJMBR2.js.map → chunk-77TNZ66J.js.map} +0 -0
@@ -12802,6 +12802,7 @@ var SyntrologieSDK = (() => {
12802
12802
  HighlightZ: () => HighlightZ,
12803
12803
  InsertHtmlZ: () => InsertHtmlZ,
12804
12804
  InsertPositionZ: () => InsertPositionZ,
12805
+ InterventionTracker: () => InterventionTracker,
12805
12806
  MAX_VISIBLE_TOASTS: () => MAX_VISIBLE_TOASTS,
12806
12807
  MatchOpZ: () => MatchOpZ,
12807
12808
  ModalContentZ: () => ModalContentZ,
@@ -20608,7 +20609,13 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
20608
20609
  }
20609
20610
 
20610
20611
  // src/fetchers/mergeConfigs.ts
20611
- function resolveVariantConfigs(client, keys, _strategy = "first-match") {
20612
+ function resolveVariantConfigs(client, keys, strategy = "merge") {
20613
+ if (strategy === "first-match") {
20614
+ return resolveFirstMatch(client, keys);
20615
+ }
20616
+ return resolveMerge(client, keys);
20617
+ }
20618
+ function resolveFirstMatch(client, keys) {
20612
20619
  for (const key of keys) {
20613
20620
  const value = client.getFeatureValue?.(key, null);
20614
20621
  if (!value || typeof value !== "object") continue;
@@ -20616,20 +20623,59 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
20616
20623
  const hasTiles = variant.tiles && variant.tiles.length > 0;
20617
20624
  const hasActions = variant.actions && variant.actions.length > 0;
20618
20625
  if (hasTiles || hasActions) {
20619
- return {
20620
- tiles: variant.tiles ?? [],
20621
- actions: variant.actions ?? [],
20622
- fetchedAt: variant.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
20623
- ...variant.schemaVersion && { schemaVersion: variant.schemaVersion },
20624
- ...variant.configVersion && { configVersion: variant.configVersion },
20625
- ...variant.canvasTitle && { canvasTitle: variant.canvasTitle },
20626
- ...variant.theme && { theme: variant.theme },
20627
- ...variant.launcher && { launcher: variant.launcher }
20628
- };
20626
+ return buildConfig([variant]);
20629
20627
  }
20630
20628
  }
20631
20629
  return null;
20632
20630
  }
20631
+ function resolveMerge(client, keys) {
20632
+ const variants = [];
20633
+ for (const key of keys) {
20634
+ const value = client.getFeatureValue?.(key, null);
20635
+ if (!value || typeof value !== "object") continue;
20636
+ const variant = value;
20637
+ const hasTiles = variant.tiles && variant.tiles.length > 0;
20638
+ const hasActions = variant.actions && variant.actions.length > 0;
20639
+ if (hasTiles || hasActions) {
20640
+ variants.push(variant);
20641
+ }
20642
+ }
20643
+ if (variants.length === 0) return null;
20644
+ return buildConfig(variants);
20645
+ }
20646
+ function buildConfig(variants) {
20647
+ const allTiles = [];
20648
+ const allActions = [];
20649
+ let fetchedAt;
20650
+ let schemaVersion;
20651
+ let configVersion;
20652
+ let canvasTitle;
20653
+ let theme;
20654
+ let launcher;
20655
+ let meta;
20656
+ for (const variant of variants) {
20657
+ if (variant.tiles) allTiles.push(...variant.tiles);
20658
+ if (variant.actions) allActions.push(...variant.actions);
20659
+ fetchedAt ?? (fetchedAt = variant.fetchedAt);
20660
+ schemaVersion ?? (schemaVersion = variant.schemaVersion);
20661
+ configVersion ?? (configVersion = variant.configVersion);
20662
+ canvasTitle ?? (canvasTitle = variant.canvasTitle);
20663
+ theme ?? (theme = variant.theme);
20664
+ launcher ?? (launcher = variant.launcher);
20665
+ meta ?? (meta = variant.meta);
20666
+ }
20667
+ return {
20668
+ tiles: allTiles,
20669
+ actions: allActions,
20670
+ fetchedAt: fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
20671
+ ...schemaVersion && { schemaVersion },
20672
+ ...configVersion && { configVersion },
20673
+ ...canvasTitle && { canvasTitle },
20674
+ ...theme && { theme },
20675
+ ...launcher && { launcher },
20676
+ ...meta && { meta }
20677
+ };
20678
+ }
20633
20679
 
20634
20680
  // src/logger.ts
20635
20681
  var debugEnabled = false;
@@ -20677,7 +20723,7 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
20677
20723
  }
20678
20724
 
20679
20725
  // src/version.ts
20680
- var SDK_VERSION = "2.12.0";
20726
+ var SDK_VERSION = "2.14.0";
20681
20727
 
20682
20728
  // src/types.ts
20683
20729
  var SDK_SCHEMA_VERSION = "2.0";
@@ -20761,9 +20807,8 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
20761
20807
  featureKey
20762
20808
  }) => {
20763
20809
  if (configUri) return configUri;
20764
- if (experiments) {
20765
- const key = featureKey ?? "smart-canvas-config";
20766
- const fromFeature = experiments.getFeatureValue?.(key, null);
20810
+ if (experiments && featureKey) {
20811
+ const fromFeature = experiments.getFeatureValue?.(featureKey, null);
20767
20812
  if (fromFeature) return fromFeature;
20768
20813
  }
20769
20814
  return void 0;
@@ -20795,9 +20840,8 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
20795
20840
  variantFlagPrefix,
20796
20841
  sdkVersion
20797
20842
  }) => async () => {
20798
- const effectiveConfigKey = configFeatureKey ?? "smart-canvas-config";
20799
- if (experiments) {
20800
- const directConfig = experiments.getFeatureValue?.(effectiveConfigKey, null);
20843
+ if (experiments && configFeatureKey) {
20844
+ const directConfig = experiments.getFeatureValue?.(configFeatureKey, null);
20801
20845
  if (directConfig && typeof directConfig === "object") {
20802
20846
  debug("SmartCanvas Config", "Resolved config directly from feature flag", directConfig);
20803
20847
  return directConfig;
@@ -21846,6 +21890,36 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
21846
21890
  return eventName.replace("$", "posthog.");
21847
21891
  }
21848
21892
  var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea"]);
21893
+ function parseElementsChain(chain) {
21894
+ if (!chain)
21895
+ return void 0;
21896
+ return chain.split(";").map((segment) => {
21897
+ const el = {};
21898
+ const colonIdx = segment.indexOf(":");
21899
+ const tagPart = colonIdx >= 0 ? segment.slice(0, colonIdx) : segment;
21900
+ const attrPart = colonIdx >= 0 ? segment.slice(colonIdx + 1) : "";
21901
+ const dotIdx = tagPart.indexOf(".");
21902
+ if (dotIdx >= 0) {
21903
+ el.tag_name = tagPart.slice(0, dotIdx);
21904
+ el.attr__class = tagPart.slice(dotIdx + 1).replace(/\./g, " ");
21905
+ } else {
21906
+ el.tag_name = tagPart;
21907
+ }
21908
+ const attrRegex = /([\w$]+)="([^"]*)"/g;
21909
+ let match;
21910
+ while ((match = attrRegex.exec(attrPart)) !== null) {
21911
+ const [, key, value] = match;
21912
+ if (key === "nth-child" || key === "nth-of-type")
21913
+ continue;
21914
+ el[key] = value;
21915
+ }
21916
+ if (el.text) {
21917
+ el.$el_text = el.text;
21918
+ delete el.text;
21919
+ }
21920
+ return el;
21921
+ });
21922
+ }
21849
21923
  function resolveInteractiveTag(elements2, directTag) {
21850
21924
  if (directTag && INTERACTIVE_TAGS.has(directTag))
21851
21925
  return directTag;
@@ -21861,7 +21935,7 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
21861
21935
  function extractProps(phEvent) {
21862
21936
  const props = {};
21863
21937
  const phProps = phEvent.properties || {};
21864
- const elements2 = phProps.$elements;
21938
+ const elements2 = phProps.$elements ?? (typeof phProps.$elements_chain === "string" ? parseElementsChain(phProps.$elements_chain) : void 0);
21865
21939
  const directTag = phProps.$tag_name ?? elements2?.[0]?.tag_name;
21866
21940
  const isClickEvent = phEvent.event === "$autocapture" || phEvent.event === "$click";
21867
21941
  props.tagName = isClickEvent ? resolveInteractiveTag(elements2, directTag) : directTag;
@@ -21869,6 +21943,9 @@ Please report this to https://github.com/markedjs/marked.`, e2) {
21869
21943
  props.elementText = phProps.$el_text;
21870
21944
  if (elements2)
21871
21945
  props.elements = elements2;
21946
+ if (isClickEvent && !elements2) {
21947
+ console.warn(`[PostHogNormalizer] $autocapture click has no element chain. PostHog may have changed wire format. Properties: $elements=${!!phProps.$elements}, $elements_chain=${typeof phProps.$elements_chain}`);
21948
+ }
21872
21949
  if (phProps.$current_url)
21873
21950
  props.url = phProps.$current_url;
21874
21951
  if (phProps.$pathname)
@@ -23454,6 +23531,19 @@ ${cssRules}
23454
23531
  }
23455
23532
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { ref: parentRef });
23456
23533
  }
23534
+ var INTERACTION_PATTERNS = [
23535
+ ":toggled",
23536
+ ":clicked",
23537
+ ":feedback",
23538
+ ":navigate",
23539
+ ":expanded",
23540
+ ":collapsed",
23541
+ ":dismissed",
23542
+ ":submitted",
23543
+ ":interacted",
23544
+ ":tip_clicked",
23545
+ ":tip_focused"
23546
+ ];
23457
23547
  function TileCard({
23458
23548
  config,
23459
23549
  surface: _surface,
@@ -23462,10 +23552,35 @@ ${cssRules}
23462
23552
  }) {
23463
23553
  const { title, subtitle, widget, props, icon } = config;
23464
23554
  const [, setTick] = (0, import_react13.useState)(0);
23555
+ const articleRef = (0, import_react13.useRef)(null);
23465
23556
  const runtime7 = useRuntime();
23466
23557
  (0, import_react13.useEffect)(() => {
23467
23558
  if (runtime7) setTick((t2) => t2 + 1);
23468
23559
  }, [runtime7]);
23560
+ (0, import_react13.useEffect)(() => {
23561
+ const tracker = typeof window !== "undefined" ? window.SynOS?.interventionTracker : null;
23562
+ if (!articleRef.current || !tracker) return;
23563
+ const observer2 = new IntersectionObserver(
23564
+ ([entry]) => {
23565
+ if (entry.isIntersecting) {
23566
+ tracker.trackSeen(config.id, config.widget ?? "unknown");
23567
+ observer2.disconnect();
23568
+ }
23569
+ },
23570
+ { threshold: 0.5 }
23571
+ );
23572
+ observer2.observe(articleRef.current);
23573
+ return () => observer2.disconnect();
23574
+ }, [config.id, config.widget]);
23575
+ (0, import_react13.useEffect)(() => {
23576
+ const tracker = typeof window !== "undefined" ? window.SynOS?.interventionTracker : null;
23577
+ if (!runtime7?.events || !tracker) return;
23578
+ return runtime7.events.subscribe((event) => {
23579
+ if (!INTERACTION_PATTERNS.some((p2) => event.name?.includes(p2))) return;
23580
+ if (event.props?.instanceId !== config.id) return;
23581
+ tracker.trackInteracted(config.id, config.widget ?? "unknown", event.name);
23582
+ });
23583
+ }, [runtime7?.events, config.id, config.widget]);
23469
23584
  const registration = (0, import_react13.useMemo)(
23470
23585
  () => runtime7?.widgets?.getRegistration?.(widget),
23471
23586
  [runtime7?.widgets, widget]
@@ -23514,6 +23629,7 @@ ${cssRules}
23514
23629
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
23515
23630
  "article",
23516
23631
  {
23632
+ ref: articleRef,
23517
23633
  "data-shadow-canvas-id": `tile-${config.id}`,
23518
23634
  style: cardStyle,
23519
23635
  onMouseEnter,
@@ -23584,7 +23700,6 @@ ${cssRules}
23584
23700
  onToggle,
23585
23701
  telemetry,
23586
23702
  launcherLabel: _launcherLabel = "Adaptives",
23587
- launcherIcon,
23588
23703
  launcherAnimate = false,
23589
23704
  launcherAnimationStyle: _launcherAnimationStyle = "pulse",
23590
23705
  notificationCount: _notificationCount,
@@ -23989,10 +24104,10 @@ ${cssRules}
23989
24104
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("path", { d: "M6 6l12 12" })
23990
24105
  ]
23991
24106
  }
23992
- ) : launcherIcon ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
24107
+ ) : config.launcher?.icon ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
23993
24108
  "img",
23994
24109
  {
23995
- src: launcherIcon,
24110
+ src: config.launcher.icon,
23996
24111
  alt: "",
23997
24112
  "aria-hidden": "true",
23998
24113
  style: {
@@ -24085,6 +24200,13 @@ ${cssRules}
24085
24200
  // src/hooks/useShadowCanvasConfig.ts
24086
24201
  var import_react15 = __toESM(require_react(), 1);
24087
24202
  var sortTiles = (tiles) => [...tiles].sort((a2, b3) => (b3.priority ?? 0) - (a2.priority ?? 0));
24203
+ function fireTriggeredForTiles(tiles) {
24204
+ const tracker = typeof window !== "undefined" ? window.SynOS?.interventionTracker : null;
24205
+ if (!tracker) return;
24206
+ for (const tile of tiles) {
24207
+ tracker.trackTriggered(tile.id, tile.widget ?? "unknown");
24208
+ }
24209
+ }
24088
24210
  function useShadowCanvasConfig({
24089
24211
  fetcher,
24090
24212
  experiments,
@@ -24107,6 +24229,7 @@ ${cssRules}
24107
24229
  if (experiments) {
24108
24230
  tiles = tiles.filter((tile) => experiments.shouldRenderRectangle(tile));
24109
24231
  }
24232
+ fireTriggeredForTiles(tiles);
24110
24233
  setState((prev) => ({ ...prev, tiles: sortTiles(tiles) }));
24111
24234
  }, [runtime7, experiments]);
24112
24235
  const load = (0, import_react15.useCallback)(async () => {
@@ -24124,6 +24247,7 @@ ${cssRules}
24124
24247
  } else if (experiments) {
24125
24248
  tiles = tiles.filter((tile) => experiments.shouldRenderRectangle(tile));
24126
24249
  }
24250
+ fireTriggeredForTiles(tiles);
24127
24251
  debug("SmartCanvas Config", `Tile count after filtering: ${tiles.length}`);
24128
24252
  const newActions = response.actions || [];
24129
24253
  const newActionsJson = JSON.stringify(newActions);
@@ -24328,6 +24452,7 @@ ${cssRules}
24328
24452
  const batchHandleRef = (0, import_react16.useRef)(initialBatchHandle ?? null);
24329
24453
  const adoptedInitialRef = (0, import_react16.useRef)(!!initialBatchHandle);
24330
24454
  const runVersionRef = (0, import_react16.useRef)(0);
24455
+ const pendingRevertRef = (0, import_react16.useRef)(null);
24331
24456
  (0, import_react16.useEffect)(() => {
24332
24457
  if (!runtime7?.actions) return;
24333
24458
  if (adoptedInitialRef.current) {
@@ -24339,6 +24464,10 @@ ${cssRules}
24339
24464
  const version = ++runVersionRef.current;
24340
24465
  const stale = () => version !== runVersionRef.current;
24341
24466
  const run = async () => {
24467
+ if (pendingRevertRef.current) {
24468
+ await pendingRevertRef.current;
24469
+ pendingRevertRef.current = null;
24470
+ }
24342
24471
  if (batchHandleRef.current) {
24343
24472
  try {
24344
24473
  await batchHandleRef.current.revertAll();
@@ -24366,7 +24495,7 @@ ${cssRules}
24366
24495
  run();
24367
24496
  return () => {
24368
24497
  if (batchHandleRef.current) {
24369
- batchHandleRef.current.revertAll().catch((err) => {
24498
+ pendingRevertRef.current = batchHandleRef.current.revertAll().catch((err) => {
24370
24499
  console.error("[SmartCanvasApp] Failed to revert actions on cleanup:", err);
24371
24500
  });
24372
24501
  batchHandleRef.current = null;
@@ -24397,7 +24526,6 @@ ${cssRules}
24397
24526
  canvasTitle: configState.canvasTitle,
24398
24527
  telemetry,
24399
24528
  launcherLabel: launcherLabel ?? configState.launcher?.label,
24400
- launcherIcon: configState.launcher?.icon,
24401
24529
  launcherAnimate: configState.launcher?.animate,
24402
24530
  launcherAnimationStyle: configState.launcher?.animationStyle,
24403
24531
  notificationCount: configState.launcher?.notificationCount ?? configState.tiles.length,
@@ -36991,32 +37119,7 @@ ${cssRules}
36991
37119
  // Capture performance metrics
36992
37120
  capture_performance: true,
36993
37121
  // Enable web vitals
36994
- enable_recording_console_log: true,
36995
- // Bootstrap callback for when flags are loaded
36996
- loaded: (ph) => {
36997
- if (enableFeatureFlags && this.featureFlagsCallback) {
36998
- ph.onFeatureFlags(() => {
36999
- const allFlags = this.getAllFeatureFlags();
37000
- if (allFlags && this.featureFlagsCallback) {
37001
- this.featureFlagsCallback(allFlags);
37002
- }
37003
- });
37004
- const existingFlags = this.getAllFeatureFlags();
37005
- if (existingFlags && Object.keys(existingFlags).length > 0) {
37006
- this.featureFlagsCallback(existingFlags);
37007
- }
37008
- }
37009
- if (this.captureCallback) {
37010
- ph.on("eventCaptured", (...args) => {
37011
- const data = args[0];
37012
- const eventName = typeof data === "string" ? data : data?.event;
37013
- const properties = typeof data === "string" ? void 0 : data?.properties;
37014
- if (typeof eventName === "string") {
37015
- this.captureCallback?.(eventName, properties);
37016
- }
37017
- });
37018
- }
37019
- }
37122
+ enable_recording_console_log: true
37020
37123
  };
37021
37124
  const result = Jo.init(
37022
37125
  options.apiKey,
@@ -37026,6 +37129,28 @@ ${cssRules}
37026
37129
  if (result) {
37027
37130
  this.client = result;
37028
37131
  }
37132
+ if (this.captureCallback && this.client) {
37133
+ this.client.on("eventCaptured", (...args) => {
37134
+ const data = args[0];
37135
+ const eventName = typeof data === "string" ? data : data?.event;
37136
+ const properties = typeof data === "string" ? void 0 : data?.properties;
37137
+ if (typeof eventName === "string") {
37138
+ this.captureCallback?.(eventName, properties);
37139
+ }
37140
+ });
37141
+ }
37142
+ if (enableFeatureFlags && this.featureFlagsCallback && this.client) {
37143
+ this.client.onFeatureFlags(() => {
37144
+ const allFlags = this.getAllFeatureFlags();
37145
+ if (allFlags && this.featureFlagsCallback) {
37146
+ this.featureFlagsCallback(allFlags);
37147
+ }
37148
+ });
37149
+ const existingFlags = this.getAllFeatureFlags();
37150
+ if (existingFlags && Object.keys(existingFlags).length > 0) {
37151
+ this.featureFlagsCallback(existingFlags);
37152
+ }
37153
+ }
37029
37154
  if (this.rrwebCallback && this.client) {
37030
37155
  this.setupRRWebIntercept();
37031
37156
  }
@@ -37150,6 +37275,55 @@ ${cssRules}
37150
37275
  return new PostHogAdapter(options);
37151
37276
  }
37152
37277
 
37278
+ // src/telemetry/InterventionTracker.ts
37279
+ var InterventionTracker = class {
37280
+ constructor(telemetry, variantId) {
37281
+ __publicField(this, "telemetry");
37282
+ __publicField(this, "variantId");
37283
+ __publicField(this, "seenSet", /* @__PURE__ */ new Set());
37284
+ __publicField(this, "triggeredSet", /* @__PURE__ */ new Set());
37285
+ this.telemetry = telemetry;
37286
+ this.variantId = variantId;
37287
+ }
37288
+ trackServed(tiles, actions) {
37289
+ this.telemetry.track?.("syntro_config_served", {
37290
+ variant_id: this.variantId,
37291
+ tiles,
37292
+ actions
37293
+ });
37294
+ }
37295
+ trackSeen(interventionId, interventionKind) {
37296
+ if (this.seenSet.has(interventionId)) return;
37297
+ this.seenSet.add(interventionId);
37298
+ this.telemetry.track?.("syntro_intervention_seen", {
37299
+ variant_id: this.variantId,
37300
+ intervention_id: interventionId,
37301
+ intervention_kind: interventionKind
37302
+ });
37303
+ }
37304
+ trackTriggered(interventionId, interventionKind) {
37305
+ if (this.triggeredSet.has(interventionId)) return;
37306
+ this.triggeredSet.add(interventionId);
37307
+ this.telemetry.track?.("syntro_intervention_triggered", {
37308
+ variant_id: this.variantId,
37309
+ intervention_id: interventionId,
37310
+ intervention_kind: interventionKind
37311
+ });
37312
+ }
37313
+ trackInteracted(interventionId, interventionKind, interactionType) {
37314
+ this.telemetry.track?.("syntro_intervention_interacted", {
37315
+ variant_id: this.variantId,
37316
+ intervention_id: interventionId,
37317
+ intervention_kind: interventionKind,
37318
+ interaction_type: interactionType
37319
+ });
37320
+ }
37321
+ resetPage() {
37322
+ this.seenSet.clear();
37323
+ this.triggeredSet.clear();
37324
+ }
37325
+ };
37326
+
37153
37327
  // src/actions/executors/core-flow.ts
37154
37328
  var executeSequence = async (action, context) => {
37155
37329
  const handles = [];
@@ -38298,15 +38472,30 @@ ${cssRules}
38298
38472
  const handles = [];
38299
38473
  const appliedHandles = [];
38300
38474
  try {
38301
- for (const action of actions) {
38302
- const handle = await apply(action);
38303
- handles.push(handle);
38304
- appliedHandles.push(handle);
38475
+ const results = await Promise.allSettled(actions.map((action) => apply(action)));
38476
+ const errors = [];
38477
+ for (const result of results) {
38478
+ if (result.status === "fulfilled") {
38479
+ handles.push(result.value);
38480
+ appliedHandles.push(result.value);
38481
+ } else {
38482
+ errors.push(
38483
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason))
38484
+ );
38485
+ }
38486
+ }
38487
+ if (errors.length > 0 && appliedHandles.length === 0) {
38488
+ throw errors[0];
38489
+ }
38490
+ if (errors.length > 0) {
38491
+ console.warn(
38492
+ `[ActionEngine] ${errors.length}/${actions.length} action(s) failed in batch:`,
38493
+ errors.map((e2) => e2.message).join("; ")
38494
+ );
38305
38495
  }
38306
38496
  } catch (error2) {
38307
38497
  console.error(
38308
- `[ActionEngine] Batch apply FAILED at action ${appliedHandles.length + 1}/${actions.length}.`,
38309
- `Successfully applied: ${appliedHandles.length}. Rolling back...`,
38498
+ `[ActionEngine] Batch apply FAILED. Successfully applied: ${appliedHandles.length}. Rolling back...`,
38310
38499
  error2
38311
38500
  );
38312
38501
  for (const handle of appliedHandles) {
@@ -38382,74 +38571,97 @@ ${cssRules}
38382
38571
  selector: external_exports.string(),
38383
38572
  route: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())])
38384
38573
  }).strict();
38574
+ var COUNTABLE_EVENTS = [
38575
+ // User interactions (from PostHog autocapture normalization)
38576
+ "ui.click",
38577
+ "ui.scroll",
38578
+ "ui.input",
38579
+ "ui.change",
38580
+ "ui.submit",
38581
+ // Behavioral detectors (from event-processor)
38582
+ "ui.hover",
38583
+ "ui.idle",
38584
+ "ui.scroll_thrash",
38585
+ "ui.focus_bounce",
38586
+ // Navigation
38587
+ "nav.page_view",
38588
+ "nav.page_leave",
38589
+ // Derived behavioral signals
38590
+ "behavior.rage_click",
38591
+ "behavior.hesitation",
38592
+ "behavior.confusion"
38593
+ ];
38594
+ var CountableEventZ = external_exports.enum(COUNTABLE_EVENTS).describe("Event name to count. ui.* = user interactions and behavioral detectors, nav.* = page navigation, behavior.* = derived behavioral signals.");
38595
+ var SESSION_METRIC_KEYS = ["time_on_page", "page_views", "scroll_depth"];
38596
+ var SessionMetricKeyZ = external_exports.enum(SESSION_METRIC_KEYS).describe("Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.");
38385
38597
  var PageUrlConditionZ = external_exports.object({
38386
38598
  type: external_exports.literal("page_url"),
38387
- url: external_exports.string()
38388
- });
38599
+ url: external_exports.string().describe('URL path to match (e.g. "/pricing", "/dashboard")')
38600
+ }).describe('Fires when the current page URL matches. Use for page-specific actions. Example: {"type": "page_url", "url": "/pricing"}');
38389
38601
  var RouteConditionZ = external_exports.object({
38390
38602
  type: external_exports.literal("route"),
38391
- routeId: external_exports.string()
38392
- });
38603
+ routeId: external_exports.string().describe("Named route ID from the route filter")
38604
+ }).describe("Fires when the current route matches a named route ID.");
38393
38605
  var AnchorVisibleConditionZ = external_exports.object({
38394
38606
  type: external_exports.literal("anchor_visible"),
38395
- anchorId: external_exports.string(),
38396
- state: external_exports.enum(["visible", "present", "absent"])
38397
- });
38607
+ anchorId: external_exports.string().describe("CSS selector of the anchor element"),
38608
+ state: external_exports.enum(["visible", "present", "absent"]).describe('"visible" = in viewport, "present" = in DOM, "absent" = not in DOM')
38609
+ }).describe(`Fires based on a DOM element's visibility state. Example: {"type": "anchor_visible", "anchorId": "#cta-button", "state": "visible"}`);
38398
38610
  var EventOccurredConditionZ = external_exports.object({
38399
38611
  type: external_exports.literal("event_occurred"),
38400
- eventName: external_exports.string(),
38401
- withinMs: external_exports.number().optional()
38402
- });
38612
+ eventName: external_exports.string().describe('Event name (e.g. "ui.click", "$pageview")'),
38613
+ withinMs: external_exports.number().optional().describe("Time window in ms. Omit = any time this session.")
38614
+ }).describe('Fires when a specific event has occurred during this session. Example: {"type": "event_occurred", "eventName": "ui.click", "withinMs": 5000}');
38403
38615
  var StateEqualsConditionZ = external_exports.object({
38404
38616
  type: external_exports.literal("state_equals"),
38405
- key: external_exports.string(),
38406
- value: external_exports.unknown()
38407
- });
38617
+ key: external_exports.string().describe("Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set()."),
38618
+ value: external_exports.unknown().describe("Expected value to match against")
38619
+ }).describe("Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.");
38408
38620
  var ViewportConditionZ = external_exports.object({
38409
38621
  type: external_exports.literal("viewport"),
38410
- minWidth: external_exports.number().optional(),
38411
- maxWidth: external_exports.number().optional(),
38412
- minHeight: external_exports.number().optional(),
38413
- maxHeight: external_exports.number().optional()
38414
- });
38622
+ minWidth: external_exports.number().optional().describe("Minimum viewport width in pixels"),
38623
+ maxWidth: external_exports.number().optional().describe("Maximum viewport width in pixels"),
38624
+ minHeight: external_exports.number().optional().describe("Minimum viewport height in pixels"),
38625
+ maxHeight: external_exports.number().optional().describe("Maximum viewport height in pixels")
38626
+ }).describe('Fires based on viewport (screen) size. Use for responsive behavior. Example: {"type": "viewport", "minWidth": 768} \u2014 fires on tablet and larger.');
38415
38627
  var SessionMetricConditionZ = external_exports.object({
38416
38628
  type: external_exports.literal("session_metric"),
38417
- key: external_exports.string(),
38629
+ key: SessionMetricKeyZ,
38418
38630
  operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
38419
- threshold: external_exports.number()
38420
- });
38631
+ threshold: external_exports.number().describe("Numeric threshold to compare against")
38632
+ }).describe('Fires when a session metric crosses a threshold. Valid keys: "time_on_page" (seconds), "page_views" (count), "scroll_depth" (0-100). Example: {"type": "session_metric", "key": "time_on_page", "operator": "gte", "threshold": 30}');
38421
38633
  var DismissedConditionZ = external_exports.object({
38422
38634
  type: external_exports.literal("dismissed"),
38423
- key: external_exports.string(),
38424
- inverted: external_exports.boolean().optional()
38425
- });
38635
+ key: external_exports.string().describe("Dismissal key (usually a tile or action ID)"),
38636
+ inverted: external_exports.boolean().optional().describe("When true, fires if NOT dismissed (default behavior)")
38637
+ }).describe("Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.");
38426
38638
  var CooldownActiveConditionZ = external_exports.object({
38427
38639
  type: external_exports.literal("cooldown_active"),
38428
- key: external_exports.string(),
38429
- inverted: external_exports.boolean().optional()
38430
- });
38640
+ key: external_exports.string().describe("Cooldown key"),
38641
+ inverted: external_exports.boolean().optional().describe("When true, fires if cooldown is NOT active")
38642
+ }).describe("Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.");
38431
38643
  var FrequencyLimitConditionZ = external_exports.object({
38432
38644
  type: external_exports.literal("frequency_limit"),
38433
- key: external_exports.string(),
38434
- limit: external_exports.number(),
38435
- inverted: external_exports.boolean().optional()
38436
- });
38645
+ key: external_exports.string().describe("Frequency counter key"),
38646
+ limit: external_exports.number().describe("Maximum allowed count"),
38647
+ inverted: external_exports.boolean().optional().describe("When true, fires if limit NOT reached")
38648
+ }).describe("Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.");
38437
38649
  var MatchOpZ = external_exports.object({
38438
38650
  equals: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).optional(),
38439
38651
  contains: external_exports.string().optional()
38440
- });
38652
+ }).describe("Match operator for counter filters. Exactly one of equals or contains must be specified.");
38441
38653
  var CounterDefZ = external_exports.object({
38442
- events: external_exports.array(external_exports.string()).min(1),
38443
- match: external_exports.record(external_exports.string(), MatchOpZ).optional()
38444
- });
38654
+ events: external_exports.array(CountableEventZ).min(1).describe("Event names to count. Use values from the countable events enum."),
38655
+ match: external_exports.record(external_exports.string(), MatchOpZ).optional().describe("Property filters. Keys are event prop names or element-chain fields (tag_name, $el_text, attr__*). All entries AND together.")
38656
+ }).describe("Defines what events to count. Registered as an accumulator predicate at config-load time.");
38445
38657
  var EventCountConditionZ = external_exports.object({
38446
38658
  type: external_exports.literal("event_count"),
38447
- key: external_exports.string(),
38659
+ key: external_exports.string().describe("Unique key for this counter (used for accumulator registration)"),
38448
38660
  operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
38449
- count: external_exports.number().int().min(0),
38450
- withinMs: external_exports.number().positive().optional(),
38451
- counter: CounterDefZ.optional()
38452
- });
38661
+ count: external_exports.number().int().min(0).describe("Target count threshold"),
38662
+ withinMs: external_exports.number().positive().optional().describe("Time window in ms. Omit = count across entire session."),
38663
+ counter: CounterDefZ.optional().describe("Inline counter definition. Defines what events to count.")
38664
+ }).describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. Example: {"type": "event_count", "key": "pricing-clicks", "operator": "gte", "count": 3, "counter": {"events": ["ui.click"], "match": {"attr__data-cta": {"contains": "pricing"}}}}');
38453
38665
  var ConditionZ = external_exports.discriminatedUnion("type", [
38454
38666
  PageUrlConditionZ,
38455
38667
  RouteConditionZ,
@@ -38464,35 +38676,35 @@ ${cssRules}
38464
38676
  EventCountConditionZ
38465
38677
  ]);
38466
38678
  var RuleZ = external_exports.object({
38467
- conditions: external_exports.array(ConditionZ),
38468
- value: external_exports.unknown()
38469
- });
38679
+ conditions: external_exports.array(ConditionZ).describe("Array of conditions \u2014 ALL must match (AND logic) for this rule to fire."),
38680
+ value: external_exports.unknown().describe("Value returned when all conditions match. For triggerWhen: true = fire the action.")
38681
+ }).describe("A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated top-to-bottom \u2014 first rule where all conditions match wins and returns its value.");
38470
38682
  var RuleStrategyZ = external_exports.object({
38471
38683
  type: external_exports.literal("rules"),
38472
- rules: external_exports.array(RuleZ),
38473
- default: external_exports.unknown()
38474
- });
38684
+ rules: external_exports.array(RuleZ).describe("Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins."),
38685
+ default: external_exports.unknown().describe("Fallback value when no rule matches. For triggerWhen: false = do not fire by default.")
38686
+ }).describe("Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match returns its value. If no rule matches, returns default. For triggerWhen: set value=true on matching rules, default=false.");
38475
38687
  var ScoreStrategyZ = external_exports.object({
38476
38688
  type: external_exports.literal("score"),
38477
38689
  field: external_exports.string(),
38478
38690
  threshold: external_exports.number(),
38479
38691
  above: external_exports.unknown(),
38480
38692
  below: external_exports.unknown()
38481
- });
38693
+ }).describe("Score-based strategy. Compares a field value against a threshold.");
38482
38694
  var ModelStrategyZ = external_exports.object({
38483
38695
  type: external_exports.literal("model"),
38484
38696
  modelId: external_exports.string(),
38485
38697
  inputs: external_exports.array(external_exports.string()),
38486
38698
  outputMapping: external_exports.record(external_exports.string(), external_exports.unknown()),
38487
38699
  default: external_exports.unknown()
38488
- });
38700
+ }).describe("ML model strategy. Sends inputs to a model and maps outputs.");
38489
38701
  var ExternalStrategyZ = external_exports.object({
38490
38702
  type: external_exports.literal("external"),
38491
38703
  endpoint: external_exports.string(),
38492
38704
  method: external_exports.enum(["GET", "POST"]).optional(),
38493
38705
  default: external_exports.unknown(),
38494
38706
  timeoutMs: external_exports.number().optional()
38495
- });
38707
+ }).describe("External API strategy. Calls an endpoint to determine the value.");
38496
38708
  var DecisionStrategyZ = external_exports.discriminatedUnion("type", [
38497
38709
  RuleStrategyZ,
38498
38710
  ScoreStrategyZ,
@@ -38588,7 +38800,8 @@ ${cssRules}
38588
38800
  color: external_exports.string().optional(),
38589
38801
  size: external_exports.string().optional(),
38590
38802
  shadow: external_exports.string().optional(),
38591
- borderRadius: external_exports.string().optional()
38803
+ borderRadius: external_exports.string().optional(),
38804
+ icon: external_exports.string().optional()
38592
38805
  });
38593
38806
  var TileElementConfigZ = external_exports.object({
38594
38807
  background: external_exports.string().optional(),
@@ -38668,7 +38881,6 @@ ${cssRules}
38668
38881
  var LauncherConfigZ = external_exports.object({
38669
38882
  enabled: external_exports.boolean().optional(),
38670
38883
  label: external_exports.string().optional(),
38671
- icon: external_exports.string().optional(),
38672
38884
  position: external_exports.string().optional(),
38673
38885
  animate: external_exports.boolean().optional(),
38674
38886
  animationStyle: external_exports.enum(["pulse", "bounce", "glow"]).optional(),
@@ -41489,7 +41701,7 @@ ${cssRules}
41489
41701
  __publicField(this, "manifestKey");
41490
41702
  __publicField(this, "variantFlagPrefix");
41491
41703
  this.client = options.client;
41492
- this.featureKey = options.featureKey ?? "smart-canvas-config";
41704
+ this.featureKey = options.featureKey;
41493
41705
  this.manifestKey = options.manifestKey;
41494
41706
  this.variantFlagPrefix = options.variantFlagPrefix;
41495
41707
  }
@@ -41906,6 +42118,26 @@ ${cssRules}
41906
42118
  const warnedAppFailures = /* @__PURE__ */ new Set();
41907
42119
  const appLoadingFetcher = baseFetcher ? async () => {
41908
42120
  const config = await baseFetcher();
42121
+ const tileCount = config.tiles?.length ?? 0;
42122
+ const actionCount = config.actions?.length ?? 0;
42123
+ const variantId = config.meta?.variant_id;
42124
+ if (tileCount > 0 || actionCount > 0) {
42125
+ if (!variantId) {
42126
+ console.warn(
42127
+ "[Syntro] Config has content but no meta.variant_id \u2014 intervention tracking disabled"
42128
+ );
42129
+ }
42130
+ }
42131
+ if (telemetry && variantId) {
42132
+ const tracker = new InterventionTracker(telemetry, variantId);
42133
+ tracker.trackServed(tileCount, actionCount);
42134
+ if (typeof window !== "undefined") {
42135
+ window.SynOS.interventionTracker = tracker;
42136
+ }
42137
+ runtime7.navigation.subscribe(() => {
42138
+ tracker.resetPage();
42139
+ });
42140
+ }
41909
42141
  console.log(
41910
42142
  "[Syntro Bootstrap] Config fetched:",
41911
42143
  `tiles=${config.tiles?.length ?? 0},`,
@@ -42005,7 +42237,7 @@ ${cssRules}
42005
42237
  }
42006
42238
 
42007
42239
  // src/index.ts
42008
- var RUNTIME_SDK_BUILD = true ? `${"2026-04-09T18:44:05.229Z"} (${"f5d1ea4ae30"})` : "dev";
42240
+ var RUNTIME_SDK_BUILD = true ? `${"2026-04-15T20:50:41.408Z"} (${"acd804e29d3"})` : "dev";
42009
42241
  if (typeof window !== "undefined") {
42010
42242
  console.log(`[Syntro Runtime] Build: ${RUNTIME_SDK_BUILD}`);
42011
42243
  const existing = window.SynOS;