fragment-headless-sdk 2.2.1 → 2.3.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.
@@ -0,0 +1,22 @@
1
+ export interface FragmentAttribution {
2
+ sectionId: string;
3
+ sectionType: 'announcement' | 'hero_banner';
4
+ timestamp: number;
5
+ }
6
+ /**
7
+ * Store attribution data for a section click
8
+ * Uses sessionStorage for session-scoped attribution (last-click wins)
9
+ * @param sectionId - The UUID of the section
10
+ * @param sectionType - The type of section (announcement or hero_banner)
11
+ */
12
+ export declare function setAttribution(sectionId: string, sectionType: string): void;
13
+ /**
14
+ * Retrieve stored attribution data
15
+ * @returns The stored attribution or null if none exists
16
+ */
17
+ export declare function getAttribution(): FragmentAttribution | null;
18
+ /**
19
+ * Clear stored attribution data
20
+ * Called after successful conversion tracking
21
+ */
22
+ export declare function clearAttribution(): void;
@@ -0,0 +1,62 @@
1
+ const STORAGE_KEY = 'fragment_attribution';
2
+ /**
3
+ * Store attribution data for a section click
4
+ * Uses sessionStorage for session-scoped attribution (last-click wins)
5
+ * @param sectionId - The UUID of the section
6
+ * @param sectionType - The type of section (announcement or hero_banner)
7
+ */
8
+ export function setAttribution(sectionId, sectionType) {
9
+ if (typeof sessionStorage === 'undefined')
10
+ return;
11
+ const attribution = {
12
+ sectionId,
13
+ sectionType: sectionType,
14
+ timestamp: Date.now()
15
+ };
16
+ try {
17
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(attribution));
18
+ }
19
+ catch (e) {
20
+ // Handle quota exceeded or other storage errors silently
21
+ console.warn('Fragment: Failed to set attribution', e);
22
+ }
23
+ }
24
+ /**
25
+ * Retrieve stored attribution data
26
+ * @returns The stored attribution or null if none exists
27
+ */
28
+ export function getAttribution() {
29
+ if (typeof sessionStorage === 'undefined')
30
+ return null;
31
+ try {
32
+ const stored = sessionStorage.getItem(STORAGE_KEY);
33
+ return stored ? JSON.parse(stored) : null;
34
+ }
35
+ catch (e) {
36
+ // Handle parse errors or other issues
37
+ console.warn('Fragment: Failed to get attribution', e);
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Clear stored attribution data
43
+ * Called after successful conversion tracking
44
+ */
45
+ export function clearAttribution() {
46
+ if (typeof sessionStorage === 'undefined')
47
+ return;
48
+ try {
49
+ sessionStorage.removeItem(STORAGE_KEY);
50
+ }
51
+ catch (e) {
52
+ console.warn('Fragment: Failed to clear attribution', e);
53
+ }
54
+ }
55
+ // Make globally accessible for Shopify theme integrations
56
+ if (typeof window !== 'undefined') {
57
+ window.fragmentAttribution = {
58
+ get: getAttribution,
59
+ set: setAttribution,
60
+ clear: clearAttribution
61
+ };
62
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./announcement-resolvers";
2
+ export * from "./attribution";
2
3
  export * from "./cache";
3
4
  export * from "./fetch-resource";
4
5
  export * from "./hero-resolvers";
@@ -1,4 +1,5 @@
1
1
  export * from "./announcement-resolvers";
2
+ export * from "./attribution";
2
3
  export * from "./cache";
3
4
  export * from "./fetch-resource";
4
5
  export * from "./hero-resolvers";
@@ -8,4 +8,8 @@ declare global {
8
8
  dataLayer?: unknown[];
9
9
  }
10
10
  }
11
+ /**
12
+ * Fire a scroll past metric when user scrolls past a section
13
+ */
14
+ export declare function fireScrollPastMetric(measurementId: string | undefined, sectionType: SectionType, sectionId: string): void;
11
15
  export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
@@ -1,4 +1,5 @@
1
1
  import { SectionType } from "../constants";
2
+ import { setAttribution } from "./attribution";
2
3
  // --- Unicode-safe Base64URL ---
3
4
  export function toBase64Url(input) {
4
5
  // Handles emojis & non-ASCII reliably:
@@ -22,6 +23,10 @@ export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId)
22
23
  return;
23
24
  if (!clickUrl)
24
25
  return;
26
+ // Store attribution for potential purchase/add-to-cart tracking
27
+ if (sectionType && sectionId) {
28
+ setAttribution(sectionId, sectionType);
29
+ }
25
30
  // Send to GA4 first (if available)
26
31
  if (measurementId && sectionType && sectionId) {
27
32
  sendGA4Event("click", measurementId, sectionType, sectionId);
@@ -55,7 +60,9 @@ export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId)
55
60
  * This allows filtering by event name in GA4 without requiring custom dimensions.
56
61
  */
57
62
  function getSectionEventName(baseEventName, sectionType) {
58
- const sectionPrefix = sectionType === SectionType.Announcement ? "fragment_announcement" : "fragment_hero_banner";
63
+ const sectionPrefix = sectionType === SectionType.Announcement
64
+ ? "fragment_announcement"
65
+ : "fragment_hero_banner";
59
66
  return `${sectionPrefix}_${baseEventName}`;
60
67
  }
61
68
  /**
@@ -65,7 +72,7 @@ function getSectionEventName(baseEventName, sectionType) {
65
72
  * Only sends section_id parameter - event name already indicates section type.
66
73
  * This is a fire-and-forget operation that won't throw errors.
67
74
  */
68
- function sendGA4Event(baseEventName, measurementId, sectionType, sectionId) {
75
+ function sendGA4Event(baseEventName, measurementId, sectionType, sectionId, additionalParams) {
69
76
  if (typeof window === "undefined")
70
77
  return;
71
78
  if (!measurementId)
@@ -76,12 +83,25 @@ function sendGA4Event(baseEventName, measurementId, sectionType, sectionId) {
76
83
  const eventName = getSectionEventName(baseEventName, sectionType);
77
84
  window.gtag("event", eventName, {
78
85
  section_id: sectionId,
86
+ ...additionalParams,
79
87
  });
80
88
  }
81
89
  catch {
82
90
  // Silently fail - don't break tracking if GA4 fails
83
91
  }
84
92
  }
93
+ /**
94
+ * Fire a scroll past metric when user scrolls past a section
95
+ */
96
+ export function fireScrollPastMetric(measurementId, sectionType, sectionId) {
97
+ if (typeof window === "undefined")
98
+ return;
99
+ if (!measurementId || !sectionType || !sectionId)
100
+ return;
101
+ sendGA4Event("scroll_past", measurementId, sectionType, sectionId, {
102
+ engagement_type: "scroll_past",
103
+ });
104
+ }
85
105
  // --- View tracking (once per element) ---
86
106
  const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
87
107
  export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionType, sectionId) {
@@ -92,6 +112,7 @@ export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionTy
92
112
  if (seenEls && seenEls.has(el))
93
113
  return; // de-dupe by element
94
114
  let fired = false;
115
+ let hasScrolledPast = false;
95
116
  const img = new Image();
96
117
  const fire = () => {
97
118
  if (fired)
@@ -119,8 +140,18 @@ export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionTy
119
140
  for (const e of entries) {
120
141
  if (e.isIntersecting && e.intersectionRatio >= 0.3) {
121
142
  fire();
143
+ // Don't disconnect - keep observing for scroll past
144
+ }
145
+ else if (!e.isIntersecting &&
146
+ !hasScrolledPast &&
147
+ e.boundingClientRect.top < 0 &&
148
+ fired) {
149
+ // User has scrolled past the section (it was visible, now it's above viewport)
150
+ hasScrolledPast = true;
151
+ if (measurementId && sectionType && sectionId) {
152
+ fireScrollPastMetric(measurementId, sectionType, sectionId);
153
+ }
122
154
  io.disconnect();
123
- break;
124
155
  }
125
156
  }
126
157
  }, { threshold: [0, 0.3] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -4,7 +4,7 @@ The official SDK for integrating with Fragment-Shopify CMS. Provides React compo
4
4
 
5
5
  ## ✨ What's New
6
6
 
7
- **v2.2.0** - Google Analytics 4 (GA4) integration with automatic section tracking
7
+ **v2.3.0** - Attribution tracking system and scroll-past engagement metrics
8
8
 
9
9
  > See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
10
10
 
@@ -73,12 +73,21 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
73
73
 
74
74
  ### Google Analytics 4 (GA4) Integration (v2.2.0+)
75
75
 
76
- - 📊 **Automatic Event Tracking**: Automatic `section_view` and `section_click` events
76
+ - 📊 **Automatic Event Tracking**: Automatic section-specific events (`fragment_announcement_view`, `fragment_hero_banner_click`, etc.)
77
77
  - 🎯 **Type-Safe Section Types**: `SectionType` enum for consistent section identification
78
78
  - 🔧 **Configurable Tracking**: Control tracking via `measurementId`, `sectionId`, and `sectionType` fields
79
79
  - ⚡ **Dual Tracking**: Maintains existing pixel tracking while adding GA4 support
80
80
  - 🛡️ **Graceful Fallback**: Works even if GA4 is not configured (doesn't break functionality)
81
81
  - 📦 **Exported Types**: `SectionType` enum available for consumer use
82
+ - 📈 **Scroll Past Tracking**: Automatic `scroll_past` events when users scroll past sections (v2.3.0+)
83
+
84
+ ### Attribution Tracking System (v2.3.0+)
85
+
86
+ - 🎯 **Last-Click Attribution**: Session-scoped attribution tracking for section interactions
87
+ - 💾 **SessionStorage Integration**: Stores attribution data in browser sessionStorage
88
+ - 🔗 **Shopify Theme Integration**: Accessible via `window.fragmentAttribution` for purchase tracking
89
+ - 📊 **Conversion Tracking**: Enables tracking which sections lead to conversions
90
+ - 🧹 **Automatic Cleanup**: Attribution data can be cleared after successful conversion tracking
82
91
 
83
92
  ---
84
93