@syntrologie/runtime-sdk 0.2.13 → 0.2.15

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.
@@ -3581,16 +3581,19 @@ var SyntrologieSDK = (() => {
3581
3581
  const opacity = Math.min(Math.max(opts?.scrimOpacity ?? 0.55, 0), 1);
3582
3582
  const ringColor = opts?.ringColor ?? "var(--syntro-ring, #5b8cff)";
3583
3583
  const blocking = opts?.blocking ?? false;
3584
+ const onClickOutside = opts?.onClickOutside ?? true;
3585
+ const onEsc = opts?.onEsc ?? true;
3584
3586
  const rootStyles = getComputedStyle(document.documentElement);
3585
3587
  const tokenScrim = rootStyles.getPropertyValue("--syntro-spotlight-backdrop").trim();
3586
3588
  const tokenRing = rootStyles.getPropertyValue("--syntro-ring").trim();
3587
3589
  const scrim = document.createElement("div");
3588
3590
  scrim.className = "syntro-spotlight-scrim";
3591
+ const needsPointerEvents = blocking || onClickOutside;
3589
3592
  Object.assign(scrim.style, {
3590
3593
  position: "fixed",
3591
3594
  inset: "0",
3592
3595
  zIndex: "2147483646",
3593
- pointerEvents: blocking ? "auto" : "none",
3596
+ pointerEvents: needsPointerEvents ? "auto" : "none",
3594
3597
  background: tokenScrim || `rgba(2, 6, 23, ${opacity})`,
3595
3598
  transition: "opacity 220ms ease",
3596
3599
  opacity: "0"
@@ -3678,15 +3681,17 @@ var SyntrologieSDK = (() => {
3678
3681
  window.addEventListener("scroll", onScroll, true);
3679
3682
  window.addEventListener("resize", onResize);
3680
3683
  const onKey = (e2) => {
3681
- if (e2.key === "Escape") handle.destroy();
3684
+ if (e2.key === "Escape" && onEsc) handle.destroy();
3682
3685
  };
3683
- window.addEventListener("keydown", onKey);
3686
+ if (onEsc) {
3687
+ window.addEventListener("keydown", onKey);
3688
+ }
3684
3689
  const onClick = (event) => {
3685
- if (!blocking) {
3686
- handle.destroy();
3687
- } else {
3690
+ if (blocking) {
3688
3691
  event.preventDefault();
3689
3692
  event.stopPropagation();
3693
+ } else if (onClickOutside) {
3694
+ handle.destroy();
3690
3695
  }
3691
3696
  };
3692
3697
  scrim.addEventListener("click", onClick);
@@ -3695,7 +3700,9 @@ var SyntrologieSDK = (() => {
3695
3700
  ro2.disconnect();
3696
3701
  window.removeEventListener("scroll", onScroll, true);
3697
3702
  window.removeEventListener("resize", onResize);
3698
- window.removeEventListener("keydown", onKey);
3703
+ if (onEsc) {
3704
+ window.removeEventListener("keydown", onKey);
3705
+ }
3699
3706
  scrim.removeEventListener("click", onClick);
3700
3707
  scrim.style.opacity = "0";
3701
3708
  setTimeout(() => {
@@ -4119,7 +4126,9 @@ var SyntrologieSDK = (() => {
4119
4126
  radiusPx: step.ring?.radiusPx,
4120
4127
  scrimOpacity: step.scrim?.opacity,
4121
4128
  ringColor: step.ringColor,
4122
- blocking: step.blocking
4129
+ blocking: step.blocking,
4130
+ onClickOutside: step.dismiss?.onClickOutside ?? true,
4131
+ onEsc: step.dismiss?.onEsc ?? true
4123
4132
  });
4124
4133
  ctx.onEvent?.("syntro_overlay_exposed", { kind: "highlight", stepId: step.id, recipeId: ctx.recipeId });
4125
4134
  const timers = [];
@@ -24545,6 +24554,7 @@ var SyntrologieSDK = (() => {
24545
24554
  CanvasRecipeZ: () => CanvasRecipeZ,
24546
24555
  HighlightStepZ: () => HighlightStepZ,
24547
24556
  SelectorZ: () => SelectorZ,
24557
+ SessionMetricTracker: () => SessionMetricTracker,
24548
24558
  ShadowCanvasOverlay: () => ShadowCanvasOverlay,
24549
24559
  SmartCanvasApp: () => SmartCanvasApp,
24550
24560
  SmartCanvasController: () => SmartCanvasController,
@@ -24558,6 +24568,7 @@ var SyntrologieSDK = (() => {
24558
24568
  createGrowthBookClient: () => createGrowthBookClient,
24559
24569
  createOverlayRecipeFetcher: () => createOverlayRecipeFetcher,
24560
24570
  createPostHogClient: () => createPostHogClient,
24571
+ createSessionMetricTracker: () => createSessionMetricTracker,
24561
24572
  createSmartCanvas: () => createSmartCanvas,
24562
24573
  createSmartCanvasController: () => createSmartCanvasController,
24563
24574
  decodeToken: () => decodeToken,
@@ -29100,14 +29111,18 @@ var SyntrologieSDK = (() => {
29100
29111
  constructor(options = {}) {
29101
29112
  this.options = options;
29102
29113
  __publicField(this, "client");
29114
+ __publicField(this, "featureFlagsCallback");
29103
29115
  this.client = options.client;
29116
+ this.featureFlagsCallback = options.onFeatureFlagsLoaded;
29104
29117
  if (!this.client && typeof window !== "undefined" && options.apiKey) {
29105
29118
  this.client = Uo;
29119
+ const enableFeatureFlags = options.enableFeatureFlags ?? true;
29106
29120
  this.client.init(options.apiKey, {
29107
29121
  api_host: options.apiHost ?? "https://posthog-dev.syntrologie.com",
29108
- // Disable feature flags - we use GrowthBook for experiments
29109
- advanced_disable_feature_flags: true,
29110
- advanced_disable_feature_flags_on_first_load: true,
29122
+ // Feature flags for segment membership (in_segment_* flags)
29123
+ // When enabled, /decide is called to get segment flags
29124
+ advanced_disable_feature_flags: !enableFeatureFlags,
29125
+ advanced_disable_feature_flags_on_first_load: !enableFeatureFlags,
29111
29126
  // Full-page tracking - all ON by default
29112
29127
  autocapture: options.autocapture ?? true,
29113
29128
  capture_pageview: options.capturePageview ?? true,
@@ -29124,10 +29139,43 @@ var SyntrologieSDK = (() => {
29124
29139
  // Capture performance metrics
29125
29140
  capture_performance: true,
29126
29141
  // Enable web vitals
29127
- enable_recording_console_log: true
29142
+ enable_recording_console_log: true,
29143
+ // Bootstrap callback for when flags are loaded
29144
+ loaded: (ph) => {
29145
+ if (enableFeatureFlags && this.featureFlagsCallback) {
29146
+ ph.onFeatureFlags(() => {
29147
+ const allFlags = this.getAllFeatureFlags();
29148
+ if (allFlags && this.featureFlagsCallback) {
29149
+ this.featureFlagsCallback(allFlags);
29150
+ }
29151
+ });
29152
+ }
29153
+ }
29128
29154
  });
29129
29155
  }
29130
29156
  }
29157
+ /**
29158
+ * Get all feature flags from PostHog.
29159
+ * Used to extract segment membership flags (in_segment_*).
29160
+ */
29161
+ getAllFeatureFlags() {
29162
+ const flags = this.client?.featureFlags?.getFlagVariants?.();
29163
+ return flags;
29164
+ }
29165
+ /**
29166
+ * Get segment membership flags (in_segment_*) from PostHog.
29167
+ */
29168
+ getSegmentFlags() {
29169
+ const allFlags = this.getAllFeatureFlags();
29170
+ if (!allFlags) return {};
29171
+ const segmentFlags = {};
29172
+ for (const [key, value] of Object.entries(allFlags)) {
29173
+ if (key.startsWith("in_segment_")) {
29174
+ segmentFlags[key] = value === true;
29175
+ }
29176
+ }
29177
+ return segmentFlags;
29178
+ }
29131
29179
  identify(id, props) {
29132
29180
  this.client?.identify(id, props);
29133
29181
  }
@@ -29176,6 +29224,9 @@ var SyntrologieSDK = (() => {
29176
29224
  setPersonPropertiesOnce(properties) {
29177
29225
  this.client?.capture("$set", { $set_once: properties });
29178
29226
  }
29227
+ getDistinctId() {
29228
+ return this.client?.get_distinct_id?.();
29229
+ }
29179
29230
  };
29180
29231
  function createPostHogClient(options = {}) {
29181
29232
  return new PostHogAdapter(options);
@@ -39170,6 +39221,151 @@ var SyntrologieSDK = (() => {
39170
39221
  };
39171
39222
  }
39172
39223
 
39224
+ // src/metrics/sessionMetrics.ts
39225
+ var STORAGE_KEY = "syntro_session_metrics";
39226
+ var SessionMetricTracker = class {
39227
+ constructor(options = {}) {
39228
+ __publicField(this, "metrics", /* @__PURE__ */ new Map());
39229
+ __publicField(this, "experiments");
39230
+ __publicField(this, "attributePrefix");
39231
+ __publicField(this, "onMetricChange");
39232
+ this.experiments = options.experiments;
39233
+ this.attributePrefix = options.attributePrefix ?? "session_";
39234
+ this.onMetricChange = options.onMetricChange;
39235
+ this.loadFromStorage();
39236
+ }
39237
+ /**
39238
+ * Increment a metric by the specified amount.
39239
+ *
39240
+ * @param metricKey - The metric to increment (e.g., "button_clicks", "page_views")
39241
+ * @param amount - Amount to increment by (default: 1)
39242
+ * @returns The new metric value
39243
+ */
39244
+ increment(metricKey, amount = 1) {
39245
+ const currentValue = this.metrics.get(metricKey) ?? 0;
39246
+ const newValue = currentValue + amount;
39247
+ this.metrics.set(metricKey, newValue);
39248
+ this.saveToStorage();
39249
+ this.updateExperimentAttributes(metricKey, newValue);
39250
+ this.onMetricChange?.(metricKey, newValue);
39251
+ return newValue;
39252
+ }
39253
+ /**
39254
+ * Set a metric to a specific value.
39255
+ *
39256
+ * @param metricKey - The metric to set
39257
+ * @param value - The value to set
39258
+ */
39259
+ set(metricKey, value) {
39260
+ this.metrics.set(metricKey, value);
39261
+ this.saveToStorage();
39262
+ this.updateExperimentAttributes(metricKey, value);
39263
+ this.onMetricChange?.(metricKey, value);
39264
+ }
39265
+ /**
39266
+ * Get the current value of a metric.
39267
+ *
39268
+ * @param metricKey - The metric to get
39269
+ * @returns The current value (0 if not set)
39270
+ */
39271
+ get(metricKey) {
39272
+ return this.metrics.get(metricKey) ?? 0;
39273
+ }
39274
+ /**
39275
+ * Check if a metric meets or exceeds a threshold.
39276
+ *
39277
+ * @param metricKey - The metric to check
39278
+ * @param threshold - The threshold value
39279
+ * @returns True if metric >= threshold
39280
+ */
39281
+ meetsThreshold(metricKey, threshold) {
39282
+ return this.get(metricKey) >= threshold;
39283
+ }
39284
+ /**
39285
+ * Get all current metric values.
39286
+ *
39287
+ * @returns Record of metric keys to values
39288
+ */
39289
+ getAll() {
39290
+ return Object.fromEntries(this.metrics);
39291
+ }
39292
+ /**
39293
+ * Reset a specific metric to zero.
39294
+ *
39295
+ * @param metricKey - The metric to reset
39296
+ */
39297
+ reset(metricKey) {
39298
+ this.metrics.delete(metricKey);
39299
+ this.saveToStorage();
39300
+ this.updateExperimentAttributes(metricKey, 0);
39301
+ this.onMetricChange?.(metricKey, 0);
39302
+ }
39303
+ /**
39304
+ * Reset all metrics (clear the session).
39305
+ */
39306
+ resetAll() {
39307
+ const keys = Array.from(this.metrics.keys());
39308
+ this.metrics.clear();
39309
+ this.saveToStorage();
39310
+ const attrs = {};
39311
+ for (const key of keys) {
39312
+ attrs[`${this.attributePrefix}${key}`] = 0;
39313
+ this.onMetricChange?.(key, 0);
39314
+ }
39315
+ this.experiments?.setAttributes?.(attrs);
39316
+ }
39317
+ /**
39318
+ * Update the experiment client (useful if experiments client changes).
39319
+ */
39320
+ setExperiments(experiments) {
39321
+ this.experiments = experiments;
39322
+ this.syncAllToExperiments();
39323
+ }
39324
+ // ==================== Private Methods ====================
39325
+ updateExperimentAttributes(metricKey, value) {
39326
+ if (!this.experiments?.setAttributes) return;
39327
+ const attributeKey = `${this.attributePrefix}${metricKey}`;
39328
+ this.experiments.setAttributes({ [attributeKey]: value });
39329
+ }
39330
+ syncAllToExperiments() {
39331
+ if (!this.experiments?.setAttributes) return;
39332
+ const attrs = {};
39333
+ for (const [key, value] of this.metrics) {
39334
+ attrs[`${this.attributePrefix}${key}`] = value;
39335
+ }
39336
+ this.experiments.setAttributes(attrs);
39337
+ }
39338
+ loadFromStorage() {
39339
+ if (typeof window === "undefined" || typeof sessionStorage === "undefined") return;
39340
+ try {
39341
+ const stored = sessionStorage.getItem(STORAGE_KEY);
39342
+ if (stored) {
39343
+ const data = JSON.parse(stored);
39344
+ for (const [key, value] of Object.entries(data)) {
39345
+ this.metrics.set(key, value.count);
39346
+ }
39347
+ }
39348
+ } catch (err) {
39349
+ console.warn("[SessionMetricTracker] Failed to load from sessionStorage:", err);
39350
+ }
39351
+ }
39352
+ saveToStorage() {
39353
+ if (typeof window === "undefined" || typeof sessionStorage === "undefined") return;
39354
+ try {
39355
+ const data = {};
39356
+ for (const [key, count] of this.metrics) {
39357
+ data[key] = { count, lastUpdated: Date.now() };
39358
+ }
39359
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
39360
+ } catch (err) {
39361
+ console.warn("[SessionMetricTracker] Failed to save to sessionStorage:", err);
39362
+ }
39363
+ }
39364
+ };
39365
+ function createSessionMetricTracker(options = {}) {
39366
+ return new SessionMetricTracker(options);
39367
+ }
39368
+
39173
39369
  // src/token.ts
39174
39370
  var TOKEN_PREFIX = "syn_";
39175
39371
  function decodeToken(token) {
@@ -39421,6 +39617,40 @@ var SyntrologieSDK = (() => {
39421
39617
  });
39422
39618
  return hasToken;
39423
39619
  }
39620
+ var SEGMENT_CACHE_KEY = "syntro_segment_attributes";
39621
+ function loadCachedSegmentAttributes() {
39622
+ if (typeof window === "undefined") return {};
39623
+ try {
39624
+ const cached = localStorage.getItem(SEGMENT_CACHE_KEY);
39625
+ if (cached) {
39626
+ const attrs = JSON.parse(cached);
39627
+ console.log("[Syntro Bootstrap] Loaded cached segment attributes:", attrs);
39628
+ return attrs;
39629
+ }
39630
+ } catch (err) {
39631
+ console.warn("[Syntro Bootstrap] Failed to load cached segment attributes:", err);
39632
+ }
39633
+ return {};
39634
+ }
39635
+ function cacheSegmentAttributes(attrs) {
39636
+ if (typeof window === "undefined") return;
39637
+ try {
39638
+ localStorage.setItem(SEGMENT_CACHE_KEY, JSON.stringify(attrs));
39639
+ console.log("[Syntro Bootstrap] Cached segment attributes:", attrs);
39640
+ } catch (err) {
39641
+ console.warn("[Syntro Bootstrap] Failed to cache segment attributes:", err);
39642
+ }
39643
+ }
39644
+ function extractSegmentFlags(allFlags) {
39645
+ if (!allFlags) return {};
39646
+ const segmentFlags = {};
39647
+ for (const [key, value] of Object.entries(allFlags)) {
39648
+ if (key.startsWith("in_segment_")) {
39649
+ segmentFlags[key] = value === true;
39650
+ }
39651
+ }
39652
+ return segmentFlags;
39653
+ }
39424
39654
  async function init(options) {
39425
39655
  console.log("[Syntro Bootstrap] ====== INIT ======");
39426
39656
  console.log("[Syntro Bootstrap] Options:", {
@@ -39451,20 +39681,41 @@ var SyntrologieSDK = (() => {
39451
39681
  const experimentHost = getEnvVar("NEXT_PUBLIC_SYNTRO_EXPERIMENT_HOST") || getEnvVar("VITE_SYNTRO_EXPERIMENT_HOST") || payload?.eh;
39452
39682
  const telemetryHost = getEnvVar("NEXT_PUBLIC_SYNTRO_TELEMETRY_HOST") || getEnvVar("VITE_SYNTRO_TELEMETRY_HOST") || payload?.th;
39453
39683
  const editorUrl = getEnvVar("NEXT_PUBLIC_SYNTRO_EDITOR_URL") || getEnvVar("VITE_SYNTRO_EDITOR_URL") || options.canvas?.editorUrl;
39684
+ const cachedSegmentAttrs = loadCachedSegmentAttributes();
39685
+ console.log("[Syntro Bootstrap] Phase 1: Using cached segment attributes:", cachedSegmentAttrs);
39686
+ let experiments;
39687
+ const onFeatureFlagsLoaded = (allFlags) => {
39688
+ console.log("[Syntro Bootstrap] Phase 2: PostHog feature flags loaded");
39689
+ const segmentFlags = extractSegmentFlags(allFlags);
39690
+ console.log("[Syntro Bootstrap] Segment flags from PostHog:", segmentFlags);
39691
+ cacheSegmentAttributes(segmentFlags);
39692
+ if (experiments) {
39693
+ const sessionAttrs = sessionMetrics?.getAll?.() ?? {};
39694
+ const updatedAttrs = { ...sessionAttrs, ...segmentFlags };
39695
+ console.log("[Syntro Bootstrap] Updating GrowthBook with attributes:", updatedAttrs);
39696
+ experiments.setAttributes?.(updatedAttrs);
39697
+ }
39698
+ };
39454
39699
  let telemetry;
39455
39700
  if (payload?.t) {
39456
39701
  telemetry = createTelemetryClient("posthog", {
39457
39702
  apiKey: payload.t,
39458
- apiHost: telemetryHost
39703
+ apiHost: telemetryHost,
39459
39704
  // undefined falls back to adapter default
39705
+ // Enable PostHog feature flags for segment membership
39706
+ enableFeatureFlags: true,
39707
+ // Wire up callback for when flags are loaded (Phase 2)
39708
+ onFeatureFlagsLoaded
39460
39709
  });
39461
39710
  }
39462
- let experiments;
39711
+ let sessionMetrics;
39463
39712
  if (payload?.e) {
39464
39713
  experiments = createExperimentClient("growthbook", {
39465
39714
  clientKey: payload.e,
39466
39715
  apiHost: experimentHost,
39467
39716
  // undefined falls back to adapter default
39717
+ // Phase 1: Use cached segment attributes for instant evaluation
39718
+ attributes: cachedSegmentAttrs,
39468
39719
  // Wire experiment tracking to telemetry provider
39469
39720
  onExperimentViewed: telemetry?.trackExperiment ? (key, variationId, variationName) => {
39470
39721
  telemetry.trackExperiment(key, variationId, variationName);
@@ -39486,13 +39737,22 @@ var SyntrologieSDK = (() => {
39486
39737
  } else if (experiments) {
39487
39738
  fetcher = createCanvasConfigFetcher({ experiments });
39488
39739
  }
39740
+ if (options.enableSessionMetrics) {
39741
+ sessionMetrics = createSessionMetricTracker({
39742
+ experiments,
39743
+ onMetricChange: (key, value) => {
39744
+ console.log(`[Syntro Bootstrap] Session metric changed: ${key} = ${value}`);
39745
+ }
39746
+ });
39747
+ console.log("[Syntro Bootstrap] SessionMetricTracker created");
39748
+ }
39489
39749
  const canvas = await createSmartCanvas({
39490
39750
  ...options.canvas,
39491
39751
  fetcher,
39492
39752
  integrations: { experiments, telemetry },
39493
39753
  editorUrl
39494
39754
  });
39495
- return { canvas, experiments, telemetry };
39755
+ return { canvas, experiments, telemetry, sessionMetrics };
39496
39756
  }
39497
39757
  var Syntro = {
39498
39758
  init,