@amplifyup/sdk 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -366,6 +366,9 @@ var AmplifyUpSDK = class {
366
366
  this.attachedEventListeners = [];
367
367
  this.trackingIntervals = [];
368
368
  this.trackingCleanup = null;
369
+ this.componentViewObservers = [];
370
+ this.componentViewSeen = /* @__PURE__ */ new Set();
371
+ this.pageEngagementActive = false;
369
372
  this.adapter = adapter || new AWSAdapter();
370
373
  if (typeof window !== "undefined") {
371
374
  this.isVisualEditorMode = this.detectVisualEditorMode();
@@ -538,6 +541,7 @@ var AmplifyUpSDK = class {
538
541
  this.initialPageSent = true;
539
542
  const url = window.location.pathname;
540
543
  this.page(url);
544
+ this.setupPageEngagement();
541
545
  }
542
546
  }
543
547
  if (this.config?.debug && this.initialized) {
@@ -570,6 +574,168 @@ var AmplifyUpSDK = class {
570
574
  }
571
575
  this.trackingIntervals.forEach((id) => clearInterval(id));
572
576
  this.trackingIntervals = [];
577
+ this.cleanupComponentViewTracking();
578
+ this.pageEngagementActive = false;
579
+ }
580
+ /** Current page path for engagement attribution. */
581
+ currentPath() {
582
+ if (typeof window === "undefined") return "/";
583
+ return window.location.pathname || "/";
584
+ }
585
+ /**
586
+ * Automatic page-level Scroll Depth + Time on Page (dwell heartbeat).
587
+ */
588
+ setupPageEngagement() {
589
+ if (typeof window === "undefined" || typeof document === "undefined") return;
590
+ if (this.isPreviewMode() || this.pageEngagementActive) return;
591
+ this.pageEngagementActive = true;
592
+ const milestones = [25, 50, 75, 100];
593
+ const reached = /* @__PURE__ */ new Set();
594
+ let visibleMs = 0;
595
+ let visibleSince = document.visibilityState === "visible" ? Date.now() : null;
596
+ let lastDwellSecondsSent = 0;
597
+ let scrollRaf = null;
598
+ const path = () => this.currentPath();
599
+ const measureScrollPercent = () => {
600
+ const doc = document.documentElement;
601
+ const body = document.body;
602
+ const scrollTop = window.scrollY || doc.scrollTop || body?.scrollTop || 0;
603
+ const scrollHeight = Math.max(
604
+ doc.scrollHeight,
605
+ body?.scrollHeight || 0,
606
+ doc.offsetHeight,
607
+ body?.offsetHeight || 0
608
+ );
609
+ const clientHeight = window.innerHeight || doc.clientHeight || 0;
610
+ const traversable = Math.max(scrollHeight - clientHeight, 0);
611
+ if (traversable <= 0) return 100;
612
+ return Math.min(100, Math.round(scrollTop / traversable * 100));
613
+ };
614
+ const checkScroll = () => {
615
+ if (this.isPreviewMode()) return;
616
+ const pct = measureScrollPercent();
617
+ for (const milestone of milestones) {
618
+ if (pct >= milestone && !reached.has(milestone)) {
619
+ reached.add(milestone);
620
+ this.track("Scroll Depth", {
621
+ percentage: milestone,
622
+ path: path()
623
+ });
624
+ }
625
+ }
626
+ };
627
+ const onScroll = () => {
628
+ if (scrollRaf != null) return;
629
+ scrollRaf = window.requestAnimationFrame(() => {
630
+ scrollRaf = null;
631
+ checkScroll();
632
+ });
633
+ };
634
+ const accumulateVisible = () => {
635
+ if (visibleSince == null) return;
636
+ visibleMs += Date.now() - visibleSince;
637
+ visibleSince = Date.now();
638
+ };
639
+ const emitDwell = (force = false) => {
640
+ if (this.isPreviewMode()) return;
641
+ accumulateVisible();
642
+ const seconds = Math.floor(visibleMs / 1e3);
643
+ if (seconds <= 0) return;
644
+ if (!force && seconds <= lastDwellSecondsSent) return;
645
+ lastDwellSecondsSent = seconds;
646
+ this.track("Time on Page", {
647
+ seconds,
648
+ path: path()
649
+ });
650
+ };
651
+ const onVisibility = () => {
652
+ if (document.visibilityState === "visible") {
653
+ visibleSince = Date.now();
654
+ } else {
655
+ emitDwell(true);
656
+ visibleSince = null;
657
+ }
658
+ };
659
+ const onPageHide = () => {
660
+ emitDwell(true);
661
+ };
662
+ window.addEventListener("scroll", onScroll, { passive: true });
663
+ document.addEventListener("visibilitychange", onVisibility);
664
+ window.addEventListener("pagehide", onPageHide);
665
+ checkScroll();
666
+ const heartbeat = setInterval(() => {
667
+ if (document.visibilityState === "visible") {
668
+ emitDwell(false);
669
+ }
670
+ }, 15e3);
671
+ this.trackingIntervals.push(heartbeat);
672
+ const previousCleanup = this.trackingCleanup;
673
+ this.trackingCleanup = () => {
674
+ window.removeEventListener("scroll", onScroll);
675
+ document.removeEventListener("visibilitychange", onVisibility);
676
+ window.removeEventListener("pagehide", onPageHide);
677
+ if (scrollRaf != null) {
678
+ window.cancelAnimationFrame(scrollRaf);
679
+ scrollRaf = null;
680
+ }
681
+ previousCleanup?.();
682
+ this.pageEngagementActive = false;
683
+ };
684
+ if (this.config?.debug) {
685
+ console.log("[AmplifyUp SDK] Page engagement tracking enabled");
686
+ }
687
+ }
688
+ /**
689
+ * Automatic Component View when component hosts enter the viewport.
690
+ */
691
+ setupComponentViewTracking() {
692
+ if (typeof document === "undefined" || typeof IntersectionObserver === "undefined") {
693
+ return;
694
+ }
695
+ if (this.isPreviewMode()) return;
696
+ this.cleanupComponentViewTracking();
697
+ const hosts = document.querySelectorAll("[data-amplifyup-component]");
698
+ if (hosts.length === 0) {
699
+ if (this.config?.debug) {
700
+ console.log("[AmplifyUp SDK] No component hosts found for Component View");
701
+ }
702
+ return;
703
+ }
704
+ const observer = new IntersectionObserver(
705
+ (entries) => {
706
+ entries.forEach((entry) => {
707
+ if (!entry.isIntersecting) return;
708
+ const el = entry.target;
709
+ const componentId = el.getAttribute("data-amplifyup-component") || el.getAttribute("data-component-id");
710
+ if (!componentId || this.componentViewSeen.has(componentId)) {
711
+ observer.unobserve(el);
712
+ return;
713
+ }
714
+ this.componentViewSeen.add(componentId);
715
+ const componentName = el.getAttribute("data-component-name") || void 0;
716
+ this.track("Component View", {
717
+ componentId,
718
+ componentName,
719
+ path: this.currentPath()
720
+ });
721
+ observer.unobserve(el);
722
+ });
723
+ },
724
+ { threshold: 0.5 }
725
+ );
726
+ hosts.forEach((host) => observer.observe(host));
727
+ this.componentViewObservers.push(observer);
728
+ if (this.config?.debug) {
729
+ console.log(
730
+ "[AmplifyUp SDK] Component View tracking enabled for",
731
+ hosts.length,
732
+ "hosts"
733
+ );
734
+ }
735
+ }
736
+ cleanupComponentViewTracking() {
737
+ this.componentViewObservers.forEach((obs) => obs.disconnect());
738
+ this.componentViewObservers = [];
573
739
  }
574
740
  /**
575
741
  * Track a page view
@@ -609,9 +775,13 @@ var AmplifyUpSDK = class {
609
775
  instanceId,
610
776
  trackingId: this.config?.trackingId,
611
777
  url: typeof window !== "undefined" ? window.location.href : void 0,
778
+ path: properties?.path || (typeof window !== "undefined" ? window.location.pathname : void 0),
612
779
  // Timestamp is included in event payload
613
780
  ...properties
614
781
  };
782
+ if (!trackProperties.path && typeof window !== "undefined") {
783
+ trackProperties.path = window.location.pathname;
784
+ }
615
785
  this.adapter.track(eventName, trackProperties);
616
786
  }
617
787
  /**
@@ -900,6 +1070,13 @@ Available Console Commands:
900
1070
  if (pageConfig?.eventTracking) {
901
1071
  this.setupEventTracking(pageConfig.eventTracking);
902
1072
  }
1073
+ if (typeof window !== "undefined") {
1074
+ window.setTimeout(() => this.setupComponentViewTracking(), 150);
1075
+ window.setTimeout(() => {
1076
+ this.setupComponentViewTracking();
1077
+ this.refreshEventTracking();
1078
+ }, 800);
1079
+ }
903
1080
  return pageConfig;
904
1081
  }
905
1082
  /**
@@ -907,10 +1084,10 @@ Available Console Commands:
907
1084
  */
908
1085
  setupEventTracking(eventConfig) {
909
1086
  if (typeof document === "undefined") return;
910
- this.cleanupEventTracking();
911
- this.eventTrackingConfig = eventConfig;
1087
+ this.detachEventListeners();
1088
+ this.eventTrackingConfig = Array.isArray(eventConfig) ? [...eventConfig] : [];
912
1089
  if (this.config?.debug) {
913
- console.log("[AmplifyUp SDK] Setting up event tracking:", eventConfig.length, "events");
1090
+ console.log("[AmplifyUp SDK] Setting up event tracking:", this.eventTrackingConfig.length, "events");
914
1091
  }
915
1092
  if (document.readyState === "loading") {
916
1093
  document.addEventListener("DOMContentLoaded", () => this.attachEventListeners());
@@ -925,19 +1102,35 @@ Available Console Commands:
925
1102
  if (typeof document === "undefined") return;
926
1103
  this.eventTrackingConfig.forEach((eventConfig) => {
927
1104
  if (eventConfig.enabled === false) return;
1105
+ if (!eventConfig.selector) {
1106
+ if (this.config?.debug) {
1107
+ console.warn(`[AmplifyUp SDK] Event ${eventConfig.eventName} has no selector, skipping`);
1108
+ }
1109
+ return;
1110
+ }
928
1111
  let targetElements = [];
929
- if (eventConfig.selector) {
930
- try {
931
- targetElements = document.querySelectorAll(eventConfig.selector);
932
- } catch (e) {
1112
+ try {
1113
+ const escapeAttr = (value) => typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(value) : value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1114
+ const scopeRoot = eventConfig.componentId ? document.querySelector(
1115
+ `[data-amplifyup-component="${escapeAttr(eventConfig.componentId)}"], [data-component-id="${escapeAttr(eventConfig.componentId)}"]`
1116
+ ) : document;
1117
+ if (!scopeRoot) {
933
1118
  if (this.config?.debug) {
934
- console.warn(`[AmplifyUp SDK] Invalid selector: ${eventConfig.selector}`, e);
1119
+ console.log(
1120
+ `[AmplifyUp SDK] Component host not found for ${eventConfig.componentId}`
1121
+ );
935
1122
  }
936
1123
  return;
937
1124
  }
938
- } else {
1125
+ targetElements = Array.from(
1126
+ scopeRoot.querySelectorAll(eventConfig.selector)
1127
+ );
1128
+ if (scopeRoot instanceof Element && scopeRoot !== document.documentElement && scopeRoot.matches?.(eventConfig.selector)) {
1129
+ targetElements.unshift(scopeRoot);
1130
+ }
1131
+ } catch (e) {
939
1132
  if (this.config?.debug) {
940
- console.warn(`[AmplifyUp SDK] Event ${eventConfig.eventName} has no selector, skipping`);
1133
+ console.warn(`[AmplifyUp SDK] Invalid selector: ${eventConfig.selector}`, e);
941
1134
  }
942
1135
  return;
943
1136
  }
@@ -993,8 +1186,15 @@ Available Console Commands:
993
1186
  handleTrackedEvent(event, config, element) {
994
1187
  const properties = {
995
1188
  eventType: config.eventType,
996
- selector: config.selector
1189
+ selector: config.selector,
1190
+ path: this.currentPath()
997
1191
  };
1192
+ if (config.componentId) {
1193
+ properties.componentId = config.componentId;
1194
+ }
1195
+ if (config.componentName) {
1196
+ properties.componentName = config.componentName;
1197
+ }
998
1198
  if (config.properties && config.properties.length > 0) {
999
1199
  config.properties.forEach((prop) => {
1000
1200
  const dataAttrValue = element.getAttribute(`data-${prop}`);
@@ -1065,9 +1265,9 @@ Available Console Commands:
1065
1265
  element._amplifyupViewObserver = observer;
1066
1266
  }
1067
1267
  /**
1068
- * Clean up all attached event listeners
1268
+ * Detach listeners/observers without clearing the saved event config.
1069
1269
  */
1070
- cleanupEventTracking() {
1270
+ detachEventListeners() {
1071
1271
  this.attachedEventListeners.forEach(({ element, eventType, handler }) => {
1072
1272
  element.removeEventListener(eventType, handler);
1073
1273
  });
@@ -1081,6 +1281,12 @@ Available Console Commands:
1081
1281
  }
1082
1282
  });
1083
1283
  }
1284
+ }
1285
+ /**
1286
+ * Clean up all attached event listeners and clear config
1287
+ */
1288
+ cleanupEventTracking() {
1289
+ this.detachEventListeners();
1084
1290
  this.eventTrackingConfig = [];
1085
1291
  if (this.config?.debug) {
1086
1292
  console.log("[AmplifyUp SDK] Cleaned up event tracking");
@@ -1090,11 +1296,13 @@ Available Console Commands:
1090
1296
  * Manually trigger event tracking setup (useful after dynamic content loads)
1091
1297
  */
1092
1298
  refreshEventTracking() {
1093
- if (this.eventTrackingConfig.length > 0) {
1094
- this.cleanupEventTracking();
1095
- const config = [...this.eventTrackingConfig];
1096
- this.eventTrackingConfig = config;
1097
- this.attachEventListeners();
1299
+ if (this.eventTrackingConfig.length === 0) return;
1300
+ const config = [...this.eventTrackingConfig];
1301
+ this.detachEventListeners();
1302
+ this.eventTrackingConfig = config;
1303
+ this.attachEventListeners();
1304
+ if (this.config?.debug) {
1305
+ console.log("[AmplifyUp SDK] Refreshed event tracking:", config.length, "events");
1098
1306
  }
1099
1307
  }
1100
1308
  /** Origins allowed to send Composer postMessage commands to the SDK. */
@@ -2087,6 +2295,7 @@ function getSDK(adapter) {
2087
2295
  return sdkInstance;
2088
2296
  }
2089
2297
  var trackContentView = (contentId, contentType, properties) => getSDK().trackContentView(contentId, contentType, properties);
2298
+ var refreshEventTracking = () => getSDK().refreshEventTracking();
2090
2299
  getSDK();
2091
2300
  function AmplifyRenderer({
2092
2301
  renderComponent,
@@ -2127,6 +2336,32 @@ function AmplifyRenderer({
2127
2336
  });
2128
2337
  }
2129
2338
  }, [pageConfig]);
2339
+ useEffect(() => {
2340
+ if (!pageConfig?.eventTracking?.length) return;
2341
+ const sdk = getSDK();
2342
+ if (!sdk.isInitialized()) return;
2343
+ sdk.setupEventTracking(pageConfig.eventTracking);
2344
+ const t1 = window.setTimeout(() => {
2345
+ sdk.setupComponentViewTracking();
2346
+ refreshEventTracking();
2347
+ }, 200);
2348
+ const t2 = window.setTimeout(() => {
2349
+ sdk.setupComponentViewTracking();
2350
+ refreshEventTracking();
2351
+ }, 1e3);
2352
+ return () => {
2353
+ window.clearTimeout(t1);
2354
+ window.clearTimeout(t2);
2355
+ };
2356
+ }, [pageConfig?.id]);
2357
+ useEffect(() => {
2358
+ if (!pageConfig) return;
2359
+ const t = window.setTimeout(() => {
2360
+ getSDK().setupComponentViewTracking();
2361
+ refreshEventTracking();
2362
+ }, 100);
2363
+ return () => window.clearTimeout(t);
2364
+ }, [variantCache, pageConfig?.id]);
2130
2365
  useEffect(() => {
2131
2366
  const isVisualEditorMode = () => {
2132
2367
  if (typeof window === "undefined") return false;
@@ -2332,6 +2567,31 @@ function AmplifyRenderer({
2332
2567
  pageConfig.layout?.header !== false;
2333
2568
  pageConfig.layout?.footer !== false;
2334
2569
  const renderNode = (node) => {
2570
+ const zoneIdFromProps = node.componentId === "AmplifyZone" ? String(node.props?.zoneId || "") : "";
2571
+ if (node.componentId === "AmplifyZone") {
2572
+ const contentSlot = node.slots?.find((s) => s.slotId === "content") || node.slots?.[0];
2573
+ const children = Array.isArray(contentSlot?.children) ? contentSlot.children : [];
2574
+ return /* @__PURE__ */ jsx(
2575
+ "div",
2576
+ {
2577
+ "data-node-id": node.id,
2578
+ "data-amplifyup-node": "",
2579
+ "data-amplifyup-component": "AmplifyZone",
2580
+ "data-amplifyup-zone": zoneIdFromProps || void 0,
2581
+ "data-zone-id": zoneIdFromProps || void 0,
2582
+ children: /* @__PURE__ */ jsx(
2583
+ "div",
2584
+ {
2585
+ "data-slot-id": "content",
2586
+ "data-amplifyup-slot": "content",
2587
+ "data-parent-node-id": node.id,
2588
+ "data-parent-component-id": "AmplifyZone",
2589
+ children: children.filter(Boolean).map((child) => /* @__PURE__ */ jsx(Fragment$1, { children: renderNode(child) }, child.id))
2590
+ }
2591
+ )
2592
+ }
2593
+ );
2594
+ }
2335
2595
  let slotChildren = {};
2336
2596
  console.log(`[AmplifyRenderer] Rendering node ${node.id} (${node.componentId})`, {
2337
2597
  hasSlots: !!(node.slots && node.slots.length > 0),