fragment-headless-sdk 2.1.9 → 2.2.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.
- package/dist/components/Announcement/AnnouncementButton.js +3 -3
- package/dist/components/Announcement/index.js +3 -3
- package/dist/components/Hero/DesktopHero.js +8 -2
- package/dist/components/Hero/MobileHero.js +8 -2
- package/dist/components/Hero/index.js +3 -2
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/section.d.ts +4 -0
- package/dist/constants/section.js +5 -0
- package/dist/types/announcement.d.ts +4 -1
- package/dist/types/hero.d.ts +4 -1
- package/dist/utils/metrics.d.ts +9 -2
- package/dist/utils/metrics.js +33 -2
- package/package.json +1 -1
- package/readme.md +10 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { ButtonType } from "../../constants";
|
|
2
|
+
import { ButtonType, SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
4
4
|
export default function AnnouncementButton({ content, buttonHref, clickHref, }) {
|
|
5
5
|
// Don’t render if no button or explicitly None
|
|
@@ -31,7 +31,7 @@ export default function AnnouncementButton({ content, buttonHref, clickHref, })
|
|
|
31
31
|
const handleClick = React.useCallback(() => {
|
|
32
32
|
if (!clickHref)
|
|
33
33
|
return;
|
|
34
|
-
fireClickMetric(clickHref);
|
|
35
|
-
}, [clickHref]);
|
|
34
|
+
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
35
|
+
}, [clickHref, content.measurementId, content.sectionType, content.sectionId]);
|
|
36
36
|
return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
|
|
37
37
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
2
2
|
import React, { useEffect, useRef } from "react";
|
|
3
|
-
import { AnnouncementType, ButtonType } from "../../constants";
|
|
3
|
+
import { AnnouncementType, ButtonType, SectionType } from "../../constants";
|
|
4
4
|
import { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
5
5
|
import AnnouncementButton from "./AnnouncementButton";
|
|
6
6
|
import { AnnouncementStyles } from "./AnnouncementStyles";
|
|
@@ -9,9 +9,9 @@ export default function Announcement({ content, type, handleClose, }) {
|
|
|
9
9
|
const ref = useRef(null);
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
if (ref.current && content.impressionUrl) {
|
|
12
|
-
fireImpressionWhenVisible(ref.current, content.impressionUrl);
|
|
12
|
+
fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
13
13
|
}
|
|
14
|
-
}, [content?.impressionUrl]);
|
|
14
|
+
}, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
15
15
|
const buttonHref = content?.buttonLink || undefined;
|
|
16
16
|
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
17
17
|
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { SectionType } from "../../constants";
|
|
2
3
|
import { fireClickMetric } from "../../utils";
|
|
3
4
|
import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
|
|
4
5
|
export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }) {
|
|
@@ -16,8 +17,13 @@ export default function DesktopHero({ buttonHref, clickHref, content, colors, co
|
|
|
16
17
|
const handleClick = React.useCallback(() => {
|
|
17
18
|
if (!clickHref)
|
|
18
19
|
return;
|
|
19
|
-
fireClickMetric(clickHref);
|
|
20
|
-
}, [
|
|
20
|
+
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
21
|
+
}, [
|
|
22
|
+
clickHref,
|
|
23
|
+
content.measurementId,
|
|
24
|
+
content.sectionType,
|
|
25
|
+
content.sectionId,
|
|
26
|
+
]);
|
|
21
27
|
return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
|
|
22
28
|
content?.videoUrl ? (React.createElement("video", { src: content.videoUrl, autoPlay: true, muted: true, loop: true, playsInline: true, className: "absolute inset-0 z-0 object-cover w-full h-full" })) : (
|
|
23
29
|
/* Image Background */
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { SectionType } from "../../constants";
|
|
2
3
|
import { fireClickMetric } from "../../utils";
|
|
3
4
|
import { renderText, } from "../../utils/hero-resolvers";
|
|
4
5
|
export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }) {
|
|
5
6
|
const handleClick = React.useCallback(() => {
|
|
6
7
|
if (!clickHref)
|
|
7
8
|
return;
|
|
8
|
-
fireClickMetric(clickHref);
|
|
9
|
-
}, [
|
|
9
|
+
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
10
|
+
}, [
|
|
11
|
+
clickHref,
|
|
12
|
+
content.measurementId,
|
|
13
|
+
content.sectionType,
|
|
14
|
+
content.sectionId,
|
|
15
|
+
]);
|
|
10
16
|
return (React.createElement("div", { className: "relative z-10 mx-auto gap-4 flex max-w-screen-md flex-col items-center justify-center py-6 text-center", style: { backgroundColor: colors.background } },
|
|
11
17
|
renderText({
|
|
12
18
|
fontSize: typography.title.fontSize,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { SectionType } from "../../constants";
|
|
2
3
|
import { buildClickUrl, fireImpressionWhenVisible, } from "../../utils";
|
|
3
4
|
import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
|
|
4
5
|
import DesktopHero from "./DesktopHero";
|
|
@@ -7,9 +8,9 @@ export default function Hero({ content }) {
|
|
|
7
8
|
const ref = useRef(null);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
if (ref.current && content.impressionUrl) {
|
|
10
|
-
fireImpressionWhenVisible(ref.current, content.impressionUrl);
|
|
11
|
+
fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
11
12
|
}
|
|
12
|
-
}, [content?.impressionUrl]);
|
|
13
|
+
}, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
13
14
|
const buttonHref = content?.buttonLink || undefined;
|
|
14
15
|
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
15
16
|
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
package/dist/constants/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnnouncementStatus, AnnouncementType, ButtonType } from "../constants";
|
|
1
|
+
import { AnnouncementStatus, AnnouncementType, ButtonType, SectionType } from "../constants";
|
|
2
2
|
import { IAnnouncementStyling } from "./styling";
|
|
3
3
|
export declare const buttonTypes: {
|
|
4
4
|
label: string;
|
|
@@ -12,6 +12,9 @@ export interface IAnnouncementContent {
|
|
|
12
12
|
counterEndDate?: string;
|
|
13
13
|
impressionUrl: string;
|
|
14
14
|
clickUrlBase: string;
|
|
15
|
+
measurementId?: string;
|
|
16
|
+
sectionId?: string;
|
|
17
|
+
sectionType?: SectionType;
|
|
15
18
|
styling?: IAnnouncementStyling;
|
|
16
19
|
}
|
|
17
20
|
export interface IAnnouncement {
|
package/dist/types/hero.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HeroStatus, HeroType } from "../constants";
|
|
1
|
+
import { HeroStatus, HeroType, SectionType } from "../constants";
|
|
2
2
|
import { IHeroStyling } from "./styling";
|
|
3
3
|
export type ShopPage = {
|
|
4
4
|
id: string;
|
|
@@ -15,6 +15,9 @@ export interface IHeroContent {
|
|
|
15
15
|
videoUrl?: string;
|
|
16
16
|
impressionUrl: string;
|
|
17
17
|
clickUrlBase: string;
|
|
18
|
+
measurementId?: string;
|
|
19
|
+
sectionId?: string;
|
|
20
|
+
sectionType?: SectionType;
|
|
18
21
|
styling?: IHeroStyling;
|
|
19
22
|
}
|
|
20
23
|
export interface IHero {
|
package/dist/utils/metrics.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { SectionType } from "../constants";
|
|
1
2
|
export declare function toBase64Url(input: string): string;
|
|
2
3
|
export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
|
|
3
|
-
export declare function fireClickMetric(clickUrl: string): void;
|
|
4
|
-
|
|
4
|
+
export declare function fireClickMetric(clickUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
gtag?: (command: string, targetId: string | Date, config?: Record<string, unknown>) => void;
|
|
8
|
+
dataLayer?: unknown[];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
package/dist/utils/metrics.js
CHANGED
|
@@ -16,11 +16,15 @@ export function buildClickUrl(clickUrlBase, targetHref) {
|
|
|
16
16
|
}
|
|
17
17
|
// Fire the click tracking URL without relying on a redirect
|
|
18
18
|
// Default to GET so legacy tracking endpoints continue to accept the request
|
|
19
|
-
export function fireClickMetric(clickUrl) {
|
|
19
|
+
export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId) {
|
|
20
20
|
if (typeof window === "undefined")
|
|
21
21
|
return;
|
|
22
22
|
if (!clickUrl)
|
|
23
23
|
return;
|
|
24
|
+
// Send to GA4 first (if available)
|
|
25
|
+
if (measurementId && sectionType && sectionId) {
|
|
26
|
+
sendGA4Event("section_click", measurementId, sectionType, sectionId);
|
|
27
|
+
}
|
|
24
28
|
try {
|
|
25
29
|
if (typeof fetch === "function") {
|
|
26
30
|
fetch(clickUrl, {
|
|
@@ -45,9 +49,32 @@ export function fireClickMetric(clickUrl) {
|
|
|
45
49
|
// nothing else we can do
|
|
46
50
|
}
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Sends a GA4 event if gtag is available and measurementId is provided.
|
|
54
|
+
* This is a fire-and-forget operation that won't throw errors.
|
|
55
|
+
*/
|
|
56
|
+
function sendGA4Event(eventName, measurementId, sectionType, sectionId) {
|
|
57
|
+
if (typeof window === "undefined")
|
|
58
|
+
return;
|
|
59
|
+
if (!measurementId)
|
|
60
|
+
return;
|
|
61
|
+
if (!window.gtag)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
window.gtag("event", eventName, {
|
|
65
|
+
section_type: sectionType,
|
|
66
|
+
section_id: sectionId,
|
|
67
|
+
event_category: "Fragment Sections",
|
|
68
|
+
event_label: `${sectionType}_${sectionId}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Silently fail - don't break tracking if GA4 fails
|
|
73
|
+
}
|
|
74
|
+
}
|
|
48
75
|
// --- View tracking (once per element) ---
|
|
49
76
|
const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
|
50
|
-
export function fireImpressionWhenVisible(el, pixelUrl) {
|
|
77
|
+
export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionType, sectionId) {
|
|
51
78
|
if (typeof window === "undefined")
|
|
52
79
|
return; // SSR guard
|
|
53
80
|
if (!el || !pixelUrl)
|
|
@@ -64,6 +91,10 @@ export function fireImpressionWhenVisible(el, pixelUrl) {
|
|
|
64
91
|
seenEls.add(el);
|
|
65
92
|
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
66
93
|
img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
|
|
94
|
+
// Also send to GA4 if available
|
|
95
|
+
if (measurementId && sectionType && sectionId) {
|
|
96
|
+
sendGA4Event("section_view", measurementId, sectionType, sectionId);
|
|
97
|
+
}
|
|
67
98
|
};
|
|
68
99
|
// Fallback if IntersectionObserver is missing
|
|
69
100
|
const fallback = () => {
|
package/package.json
CHANGED
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.
|
|
7
|
+
**v2.2.0** - Google Analytics 4 (GA4) integration with automatic section tracking
|
|
8
8
|
|
|
9
9
|
> See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
|
|
10
10
|
|
|
@@ -71,6 +71,15 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
|
|
|
71
71
|
- 🌐 **Cross-Browser**: Works across all modern browsers with appropriate fallbacks
|
|
72
72
|
- 🔄 **Graceful Degradation**: Tracking fails silently without affecting user experience
|
|
73
73
|
|
|
74
|
+
### Google Analytics 4 (GA4) Integration (v2.2.0+)
|
|
75
|
+
|
|
76
|
+
- 📊 **Automatic Event Tracking**: Automatic `section_view` and `section_click` events
|
|
77
|
+
- 🎯 **Type-Safe Section Types**: `SectionType` enum for consistent section identification
|
|
78
|
+
- 🔧 **Configurable Tracking**: Control tracking via `measurementId`, `sectionId`, and `sectionType` fields
|
|
79
|
+
- ⚡ **Dual Tracking**: Maintains existing pixel tracking while adding GA4 support
|
|
80
|
+
- 🛡️ **Graceful Fallback**: Works even if GA4 is not configured (doesn't break functionality)
|
|
81
|
+
- 📦 **Exported Types**: `SectionType` enum available for consumer use
|
|
82
|
+
|
|
74
83
|
---
|
|
75
84
|
|
|
76
85
|
## 📦 Installation
|