fragment-headless-sdk 2.3.0 β 2.3.2
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.d.ts +1 -2
- package/dist/components/Announcement/AnnouncementButton.js +3 -5
- package/dist/components/Announcement/index.js +7 -9
- package/dist/components/Hero/DesktopHero.d.ts +1 -2
- package/dist/components/Hero/DesktopHero.js +3 -10
- package/dist/components/Hero/MobileHero.d.ts +1 -2
- package/dist/components/Hero/MobileHero.js +3 -10
- package/dist/components/Hero/index.js +7 -9
- package/dist/types/announcement.d.ts +0 -2
- package/dist/types/hero.d.ts +0 -2
- package/dist/utils/attribution.d.ts +13 -3
- package/dist/utils/attribution.js +30 -3
- package/dist/utils/cache.js +5 -3
- package/dist/utils/metrics.d.ts +2 -4
- package/dist/utils/metrics.js +7 -57
- package/docs/CHANGELOG.md +555 -0
- package/package.json +29 -5
- package/readme.md +115 -14
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { IAnnouncementContent } from "../../types";
|
|
3
|
-
export default function AnnouncementButton({ content, buttonHref,
|
|
3
|
+
export default function AnnouncementButton({ content, buttonHref, }: {
|
|
4
4
|
content: IAnnouncementContent;
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
}): React.JSX.Element | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ButtonType, SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
4
|
-
export default function AnnouncementButton({ content, buttonHref,
|
|
4
|
+
export default function AnnouncementButton({ content, buttonHref, }) {
|
|
5
5
|
// Donβt render if no button or explicitly None
|
|
6
6
|
if (!content?.buttonText || content.buttonType === ButtonType.None)
|
|
7
7
|
return null;
|
|
@@ -29,9 +29,7 @@ export default function AnnouncementButton({ content, buttonHref, clickHref, })
|
|
|
29
29
|
delete attributes["aria-label"];
|
|
30
30
|
}
|
|
31
31
|
const handleClick = React.useCallback(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
35
|
-
}, [clickHref, content.measurementId, content.sectionType, content.sectionId]);
|
|
32
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
33
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
36
34
|
return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
|
|
37
35
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
2
3
|
import React, { useEffect, useRef } from "react";
|
|
3
4
|
import { AnnouncementType, ButtonType, SectionType } from "../../constants";
|
|
4
|
-
import {
|
|
5
|
+
import { fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
5
6
|
import AnnouncementButton from "./AnnouncementButton";
|
|
6
7
|
import { AnnouncementStyles } from "./AnnouncementStyles";
|
|
7
8
|
import CountdownTimer from "./CountdownTimer";
|
|
8
9
|
export default function Announcement({ content, type, handleClose, }) {
|
|
9
10
|
const ref = useRef(null);
|
|
10
11
|
useEffect(() => {
|
|
11
|
-
if (ref.current
|
|
12
|
-
fireImpressionWhenVisible(ref.current, content.
|
|
12
|
+
if (ref.current) {
|
|
13
|
+
fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
13
14
|
}
|
|
14
|
-
}, [content?.
|
|
15
|
+
}, [content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
15
16
|
const buttonHref = content?.buttonLink || undefined;
|
|
16
|
-
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
17
|
-
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
18
|
-
: undefined;
|
|
19
17
|
if (!content)
|
|
20
18
|
return null;
|
|
21
19
|
const styling = content.styling;
|
|
@@ -60,13 +58,13 @@ export default function Announcement({ content, type, handleClose, }) {
|
|
|
60
58
|
React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
|
|
61
59
|
__html: content.announcementHtml || "",
|
|
62
60
|
} }))),
|
|
63
|
-
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref
|
|
61
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
|
|
64
62
|
React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
|
|
65
63
|
React.createElement("div", { dangerouslySetInnerHTML: {
|
|
66
64
|
__html: content.announcementHtml || "",
|
|
67
65
|
} })),
|
|
68
66
|
type === AnnouncementType.Countdown && (React.createElement(CountdownTimer, { content: content })),
|
|
69
|
-
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref
|
|
67
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))),
|
|
70
68
|
React.createElement("div", { onClick: handleClose, className: closeButtonClass, style: closeButtonStyle, ...(closeButtonAttributes ?? {}) },
|
|
71
69
|
React.createElement(XMarkIcon, { className: "w-5 h-5" })))));
|
|
72
70
|
}
|
|
@@ -3,7 +3,6 @@ import { IHeroContent } from "../../types";
|
|
|
3
3
|
import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
|
|
4
4
|
interface HeroViewProps {
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
content: IHeroContent;
|
|
8
7
|
colors: ReturnType<typeof resolveHeroColors>;
|
|
9
8
|
contentWidthClass: string;
|
|
@@ -11,5 +10,5 @@ interface HeroViewProps {
|
|
|
11
10
|
position: "left" | "center" | "right";
|
|
12
11
|
height: string;
|
|
13
12
|
}
|
|
14
|
-
export default function DesktopHero({ buttonHref,
|
|
13
|
+
export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
|
|
15
14
|
export {};
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric } from "../../utils";
|
|
4
4
|
import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
|
|
5
|
-
export default function DesktopHero({ buttonHref,
|
|
5
|
+
export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }) {
|
|
6
6
|
const getPositionClasses = () => {
|
|
7
7
|
switch (position) {
|
|
8
8
|
case "center":
|
|
@@ -15,15 +15,8 @@ export default function DesktopHero({ buttonHref, clickHref, content, colors, co
|
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
const handleClick = React.useCallback(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
21
|
-
}, [
|
|
22
|
-
clickHref,
|
|
23
|
-
content.measurementId,
|
|
24
|
-
content.sectionType,
|
|
25
|
-
content.sectionId,
|
|
26
|
-
]);
|
|
18
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
19
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
27
20
|
return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
|
|
28
21
|
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" })) : (
|
|
29
22
|
/* Image Background */
|
|
@@ -3,10 +3,9 @@ import { IHeroContent } from "../../types";
|
|
|
3
3
|
import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
|
|
4
4
|
interface MobileHeroProps {
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
content: IHeroContent;
|
|
8
7
|
colors: ReturnType<typeof resolveHeroColors>;
|
|
9
8
|
typography: ReturnType<typeof resolveHeroTypography>;
|
|
10
9
|
}
|
|
11
|
-
export default function MobileHero({ buttonHref,
|
|
10
|
+
export default function MobileHero({ buttonHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
|
|
12
11
|
export {};
|
|
@@ -2,17 +2,10 @@ import React from "react";
|
|
|
2
2
|
import { SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric } from "../../utils";
|
|
4
4
|
import { renderText, } from "../../utils/hero-resolvers";
|
|
5
|
-
export default function MobileHero({ buttonHref,
|
|
5
|
+
export default function MobileHero({ buttonHref, content, colors, typography, }) {
|
|
6
6
|
const handleClick = React.useCallback(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
10
|
-
}, [
|
|
11
|
-
clickHref,
|
|
12
|
-
content.measurementId,
|
|
13
|
-
content.sectionType,
|
|
14
|
-
content.sectionId,
|
|
15
|
-
]);
|
|
7
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
8
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
16
9
|
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 } },
|
|
17
10
|
renderText({
|
|
18
11
|
fontSize: typography.title.fontSize,
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useEffect, useRef } from "react";
|
|
2
3
|
import { SectionType } from "../../constants";
|
|
3
|
-
import {
|
|
4
|
+
import { fireImpressionWhenVisible, } from "../../utils";
|
|
4
5
|
import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
|
|
5
6
|
import DesktopHero from "./DesktopHero";
|
|
6
7
|
import MobileHero from "./MobileHero";
|
|
7
8
|
export default function Hero({ content }) {
|
|
8
9
|
const ref = useRef(null);
|
|
9
10
|
useEffect(() => {
|
|
10
|
-
if (ref.current
|
|
11
|
-
fireImpressionWhenVisible(ref.current, content.
|
|
11
|
+
if (ref.current) {
|
|
12
|
+
fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
12
13
|
}
|
|
13
|
-
}, [content?.
|
|
14
|
+
}, [content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
14
15
|
const buttonHref = content?.buttonLink || undefined;
|
|
15
|
-
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
16
|
-
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
17
|
-
: undefined;
|
|
18
16
|
if (!content)
|
|
19
17
|
return null;
|
|
20
18
|
const colors = resolveHeroColors(content);
|
|
@@ -24,7 +22,7 @@ export default function Hero({ content }) {
|
|
|
24
22
|
const height = resolveHeight(content);
|
|
25
23
|
return (React.createElement("div", { className: "bg-black", ref: ref, style: { backgroundColor: colors.background } },
|
|
26
24
|
React.createElement("div", { className: "hidden lg:block" },
|
|
27
|
-
React.createElement(DesktopHero, { content: content, buttonHref: buttonHref,
|
|
25
|
+
React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
|
|
28
26
|
React.createElement("div", { className: "block lg:hidden" },
|
|
29
|
-
React.createElement(MobileHero, { content: content, buttonHref: buttonHref,
|
|
27
|
+
React.createElement(MobileHero, { content: content, buttonHref: buttonHref, colors: colors, typography: typography }))));
|
|
30
28
|
}
|
package/dist/types/hero.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { SectionType } from '../constants';
|
|
1
2
|
export interface FragmentAttribution {
|
|
2
3
|
sectionId: string;
|
|
3
|
-
sectionType:
|
|
4
|
+
sectionType: SectionType;
|
|
4
5
|
timestamp: number;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
@@ -9,10 +10,10 @@ export interface FragmentAttribution {
|
|
|
9
10
|
* @param sectionId - The UUID of the section
|
|
10
11
|
* @param sectionType - The type of section (announcement or hero_banner)
|
|
11
12
|
*/
|
|
12
|
-
export declare function setAttribution(sectionId: string, sectionType:
|
|
13
|
+
export declare function setAttribution(sectionId: string, sectionType: SectionType): void;
|
|
13
14
|
/**
|
|
14
15
|
* Retrieve stored attribution data
|
|
15
|
-
* @returns The stored attribution or null if none exists
|
|
16
|
+
* @returns The stored attribution or null if none exists or data is invalid
|
|
16
17
|
*/
|
|
17
18
|
export declare function getAttribution(): FragmentAttribution | null;
|
|
18
19
|
/**
|
|
@@ -20,3 +21,12 @@ export declare function getAttribution(): FragmentAttribution | null;
|
|
|
20
21
|
* Called after successful conversion tracking
|
|
21
22
|
*/
|
|
22
23
|
export declare function clearAttribution(): void;
|
|
24
|
+
declare global {
|
|
25
|
+
interface Window {
|
|
26
|
+
fragmentAttribution?: {
|
|
27
|
+
get: () => FragmentAttribution | null;
|
|
28
|
+
set: (sectionId: string, sectionType: SectionType) => void;
|
|
29
|
+
clear: () => void;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SectionType } from '../constants';
|
|
1
2
|
const STORAGE_KEY = 'fragment_attribution';
|
|
2
3
|
/**
|
|
3
4
|
* Store attribution data for a section click
|
|
@@ -10,7 +11,7 @@ export function setAttribution(sectionId, sectionType) {
|
|
|
10
11
|
return;
|
|
11
12
|
const attribution = {
|
|
12
13
|
sectionId,
|
|
13
|
-
sectionType
|
|
14
|
+
sectionType,
|
|
14
15
|
timestamp: Date.now()
|
|
15
16
|
};
|
|
16
17
|
try {
|
|
@@ -21,20 +22,46 @@ export function setAttribution(sectionId, sectionType) {
|
|
|
21
22
|
console.warn('Fragment: Failed to set attribution', e);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates that parsed data matches the FragmentAttribution interface
|
|
27
|
+
*/
|
|
28
|
+
function isValidAttribution(data) {
|
|
29
|
+
if (!data || typeof data !== 'object')
|
|
30
|
+
return false;
|
|
31
|
+
const obj = data;
|
|
32
|
+
return (typeof obj.sectionId === 'string' &&
|
|
33
|
+
(obj.sectionType === SectionType.Announcement || obj.sectionType === SectionType.HeroBanner) &&
|
|
34
|
+
typeof obj.timestamp === 'number');
|
|
35
|
+
}
|
|
24
36
|
/**
|
|
25
37
|
* Retrieve stored attribution data
|
|
26
|
-
* @returns The stored attribution or null if none exists
|
|
38
|
+
* @returns The stored attribution or null if none exists or data is invalid
|
|
27
39
|
*/
|
|
28
40
|
export function getAttribution() {
|
|
29
41
|
if (typeof sessionStorage === 'undefined')
|
|
30
42
|
return null;
|
|
31
43
|
try {
|
|
32
44
|
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
33
|
-
|
|
45
|
+
if (!stored)
|
|
46
|
+
return null;
|
|
47
|
+
const parsed = JSON.parse(stored);
|
|
48
|
+
if (!isValidAttribution(parsed)) {
|
|
49
|
+
// Invalid data - clear it and return null
|
|
50
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
34
54
|
}
|
|
35
55
|
catch (e) {
|
|
36
56
|
// Handle parse errors or other issues
|
|
37
57
|
console.warn('Fragment: Failed to get attribution', e);
|
|
58
|
+
// Clear potentially corrupted data
|
|
59
|
+
try {
|
|
60
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore errors when clearing
|
|
64
|
+
}
|
|
38
65
|
return null;
|
|
39
66
|
}
|
|
40
67
|
}
|
package/dist/utils/cache.js
CHANGED
|
@@ -15,12 +15,14 @@ export async function revalidateFragmentCache(tags) {
|
|
|
15
15
|
try {
|
|
16
16
|
// Dynamic import to avoid issues in non-Next.js environments
|
|
17
17
|
// @ts-ignore - next/cache may not be available in all environments
|
|
18
|
-
const
|
|
18
|
+
const mod = await import("next/cache");
|
|
19
|
+
// Cast: Next.js types vary by version (1 arg vs 2 arg with profile). Pass tag + "max" for Next 16+.
|
|
20
|
+
const revalidateTag = mod.revalidateTag;
|
|
19
21
|
if (tags && tags.length > 0) {
|
|
20
22
|
// Revalidate specific tags
|
|
21
23
|
tags.forEach((tag) => {
|
|
22
24
|
try {
|
|
23
|
-
revalidateTag(tag);
|
|
25
|
+
revalidateTag(tag, "max");
|
|
24
26
|
}
|
|
25
27
|
catch (err) {
|
|
26
28
|
console.warn(`Failed to revalidate tag ${tag}:`, err);
|
|
@@ -31,7 +33,7 @@ export async function revalidateFragmentCache(tags) {
|
|
|
31
33
|
// Revalidate all Fragment resource types
|
|
32
34
|
Object.values(ResourceType).forEach((type) => {
|
|
33
35
|
try {
|
|
34
|
-
revalidateTag(`fragment-${type}
|
|
36
|
+
revalidateTag(`fragment-${type}`, "max");
|
|
35
37
|
}
|
|
36
38
|
catch (err) {
|
|
37
39
|
console.warn(`Failed to revalidate fragment-${type}:`, err);
|
package/dist/utils/metrics.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { SectionType } from "../constants";
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
|
|
4
|
-
export declare function fireClickMetric(clickUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
|
2
|
+
export declare function fireClickMetric(measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
|
5
3
|
declare global {
|
|
6
4
|
interface Window {
|
|
7
5
|
gtag?: (command: string, targetId: string | Date, config?: Record<string, unknown>) => void;
|
|
@@ -12,4 +10,4 @@ declare global {
|
|
|
12
10
|
* Fire a scroll past metric when user scrolls past a section
|
|
13
11
|
*/
|
|
14
12
|
export declare function fireScrollPastMetric(measurementId: string | undefined, sectionType: SectionType, sectionId: string): void;
|
|
15
|
-
export declare function fireImpressionWhenVisible(el: HTMLElement,
|
|
13
|
+
export declare function fireImpressionWhenVisible(el: HTMLElement, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
package/dist/utils/metrics.js
CHANGED
|
@@ -1,68 +1,22 @@
|
|
|
1
1
|
import { SectionType } from "../constants";
|
|
2
2
|
import { setAttribution } from "./attribution";
|
|
3
|
-
|
|
4
|
-
export function toBase64Url(input) {
|
|
5
|
-
// Handles emojis & non-ASCII reliably:
|
|
6
|
-
const b64 = btoa(encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
|
|
7
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
-
}
|
|
9
|
-
// --- Robust query appender ---
|
|
10
|
-
function appendQuery(url, key, value) {
|
|
11
|
-
const sep = url.includes("?") ? "&" : "?";
|
|
12
|
-
return `${url}${sep}${key}=${value}`;
|
|
13
|
-
}
|
|
14
|
-
// Build the tracking URL that encodes the final destination for metrics
|
|
15
|
-
export function buildClickUrl(clickUrlBase, targetHref) {
|
|
16
|
-
const u = encodeURIComponent(toBase64Url(targetHref));
|
|
17
|
-
return appendQuery(clickUrlBase, "u", u);
|
|
18
|
-
}
|
|
19
|
-
// Fire the click tracking URL without relying on a redirect
|
|
20
|
-
// Default to GET so legacy tracking endpoints continue to accept the request
|
|
21
|
-
export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId) {
|
|
3
|
+
export function fireClickMetric(measurementId, sectionType, sectionId) {
|
|
22
4
|
if (typeof window === "undefined")
|
|
23
5
|
return;
|
|
24
|
-
|
|
25
|
-
return;
|
|
26
|
-
// Store attribution for potential purchase/add-to-cart tracking
|
|
6
|
+
// Store attribution for potential purchase/add-to-cart
|
|
27
7
|
if (sectionType && sectionId) {
|
|
28
8
|
setAttribution(sectionId, sectionType);
|
|
29
9
|
}
|
|
30
|
-
// Send to GA4 first (if available)
|
|
31
10
|
if (measurementId && sectionType && sectionId) {
|
|
32
11
|
sendGA4Event("click", measurementId, sectionType, sectionId);
|
|
33
12
|
}
|
|
34
|
-
try {
|
|
35
|
-
if (typeof fetch === "function") {
|
|
36
|
-
fetch(clickUrl, {
|
|
37
|
-
method: "GET",
|
|
38
|
-
mode: "no-cors",
|
|
39
|
-
keepalive: true,
|
|
40
|
-
}).catch(() => {
|
|
41
|
-
/* no-op */
|
|
42
|
-
});
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// swallow and fall back to <img>
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const img = new Image();
|
|
51
|
-
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
52
|
-
img.src = clickUrl;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// nothing else we can do
|
|
56
|
-
}
|
|
57
13
|
}
|
|
58
14
|
/**
|
|
59
15
|
* Generates a section-specific event name for GA4 tracking.
|
|
60
16
|
* This allows filtering by event name in GA4 without requiring custom dimensions.
|
|
61
17
|
*/
|
|
62
18
|
function getSectionEventName(baseEventName, sectionType) {
|
|
63
|
-
const sectionPrefix = sectionType === SectionType.Announcement
|
|
64
|
-
? "fragment_announcement"
|
|
65
|
-
: "fragment_hero_banner";
|
|
19
|
+
const sectionPrefix = sectionType === SectionType.Announcement ? "fragment_announcement" : "fragment_hero_banner";
|
|
66
20
|
return `${sectionPrefix}_${baseEventName}`;
|
|
67
21
|
}
|
|
68
22
|
/**
|
|
@@ -104,25 +58,21 @@ export function fireScrollPastMetric(measurementId, sectionType, sectionId) {
|
|
|
104
58
|
}
|
|
105
59
|
// --- View tracking (once per element) ---
|
|
106
60
|
const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
|
107
|
-
export function fireImpressionWhenVisible(el,
|
|
61
|
+
export function fireImpressionWhenVisible(el, measurementId, sectionType, sectionId) {
|
|
108
62
|
if (typeof window === "undefined")
|
|
109
63
|
return; // SSR guard
|
|
110
|
-
if (!el
|
|
64
|
+
if (!el)
|
|
111
65
|
return;
|
|
112
66
|
if (seenEls && seenEls.has(el))
|
|
113
67
|
return; // de-dupe by element
|
|
114
68
|
let fired = false;
|
|
115
69
|
let hasScrolledPast = false;
|
|
116
|
-
const img = new Image();
|
|
117
70
|
const fire = () => {
|
|
118
71
|
if (fired)
|
|
119
72
|
return;
|
|
120
73
|
fired = true;
|
|
121
74
|
if (seenEls)
|
|
122
75
|
seenEls.add(el);
|
|
123
|
-
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
124
|
-
img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
|
|
125
|
-
// Also send to GA4 if available
|
|
126
76
|
if (measurementId && sectionType && sectionId) {
|
|
127
77
|
sendGA4Event("view", measurementId, sectionType, sectionId);
|
|
128
78
|
}
|
|
@@ -140,18 +90,18 @@ export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionTy
|
|
|
140
90
|
for (const e of entries) {
|
|
141
91
|
if (e.isIntersecting && e.intersectionRatio >= 0.3) {
|
|
142
92
|
fire();
|
|
143
|
-
// Don't disconnect - keep observing for scroll past
|
|
144
93
|
}
|
|
145
94
|
else if (!e.isIntersecting &&
|
|
146
95
|
!hasScrolledPast &&
|
|
147
96
|
e.boundingClientRect.top < 0 &&
|
|
148
97
|
fired) {
|
|
149
|
-
// User
|
|
98
|
+
// User scrolled past the section (it's above viewport and was previously visible)
|
|
150
99
|
hasScrolledPast = true;
|
|
151
100
|
if (measurementId && sectionType && sectionId) {
|
|
152
101
|
fireScrollPastMetric(measurementId, sectionType, sectionId);
|
|
153
102
|
}
|
|
154
103
|
io.disconnect();
|
|
104
|
+
break;
|
|
155
105
|
}
|
|
156
106
|
}
|
|
157
107
|
}, { threshold: [0, 0.3] });
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
### [Unreleased]
|
|
9
|
+
|
|
10
|
+
#### Removed
|
|
11
|
+
|
|
12
|
+
### [2.3.2] - 2026-02-27
|
|
13
|
+
|
|
14
|
+
#### Removed
|
|
15
|
+
|
|
16
|
+
- **Legacy server-side tracking** β The previous pixel/click tracking system has been fully replaced by Google Analytics 4 (GA4):
|
|
17
|
+
- `impressionUrl` and `clickUrlBase` on content objects are no longer used; the API now provides `measurementId`, `sectionId`, and `sectionType` for GA4 events.
|
|
18
|
+
- The `makeSignedMetricUrls` helper and server-side `/api/v1/t` endpoints (impression/click) have been removed from the Fragment app; the SDK sends view/click/scroll_past via `gtag` only.
|
|
19
|
+
- See [2.2.0] for GA4 integration details. Historical entries [1.0.5] and earlier described the old pixel-based flow.
|
|
20
|
+
|
|
21
|
+
### [2.3.1] - 2026-01-25
|
|
22
|
+
|
|
23
|
+
### π§ Technical Improvements
|
|
24
|
+
|
|
25
|
+
- **Enhanced Error Handling** β Improved error handling in attribution functions
|
|
26
|
+
- Better error messages and logging
|
|
27
|
+
- Automatic cleanup of corrupted data
|
|
28
|
+
- Graceful degradation when sessionStorage is unavailable
|
|
29
|
+
|
|
30
|
+
## [2.3.0] - 2026-01-25
|
|
31
|
+
|
|
32
|
+
### β¨ Attribution Tracking System
|
|
33
|
+
|
|
34
|
+
- **Session-Based Attribution** β New attribution tracking system for purchase/add-to-cart attribution
|
|
35
|
+
- Automatically stores attribution data when users click section buttons
|
|
36
|
+
- Uses sessionStorage for session-scoped attribution (last-click wins)
|
|
37
|
+
- Tracks section ID, section type, and timestamp for each click
|
|
38
|
+
|
|
39
|
+
### π Documentation
|
|
40
|
+
|
|
41
|
+
- **New Attribution Section** β Added comprehensive documentation for attribution tracking
|
|
42
|
+
- Usage examples for Shopify theme integrations
|
|
43
|
+
- Programmatic access examples
|
|
44
|
+
- Use cases for purchase and add-to-cart attribution
|
|
45
|
+
|
|
46
|
+
## [2.2.0] - 2025-01-18
|
|
47
|
+
|
|
48
|
+
### β¨ Google Analytics 4 (GA4) Integration
|
|
49
|
+
|
|
50
|
+
- **GA4 Event Tracking** β Added comprehensive Google Analytics 4 integration for section tracking
|
|
51
|
+
- Automatic `section_view` events when sections become visible
|
|
52
|
+
- Automatic `section_click` events when users interact with buttons
|
|
53
|
+
- Configurable via `measurementId`, `sectionId`, and `sectionType` fields
|
|
54
|
+
- Graceful fallback if GA4 is not available (doesn't break functionality)
|
|
55
|
+
|
|
56
|
+
## [2.1.9] - 2025-12-26
|
|
57
|
+
|
|
58
|
+
### β¨ Announcement Resolvers System
|
|
59
|
+
|
|
60
|
+
- **New Announcement Resolvers Utility** β Added centralized color resolution system for Announcement components
|
|
61
|
+
- `resolveAnnouncementColors()` β Intelligent color resolution with guaranteed fallback values
|
|
62
|
+
- `resolveCountdownColors()` β Dedicated color resolver for countdown timer styling
|
|
63
|
+
- Supports both new format (`colors.background`) and legacy format (`bgColor`) for backward compatibility
|
|
64
|
+
|
|
65
|
+
### π§ Component Refactoring
|
|
66
|
+
|
|
67
|
+
- **Centralized Color Resolution** β Refactored all Announcement components to use the new resolver system
|
|
68
|
+
- Improved code maintainability and consistency across all announcement types
|
|
69
|
+
|
|
70
|
+
### π¨ Countdown Timer Styling Improvements
|
|
71
|
+
|
|
72
|
+
- **Enhanced Layout** β Improved visual spacing and sizing
|
|
73
|
+
- Added `gap-1` between countdown digits
|
|
74
|
+
- Reduced digit font size from `text-xl` to `text-lg`
|
|
75
|
+
- Changed line height from `leading-none` to `leading-tight`
|
|
76
|
+
|
|
77
|
+
### π§ Type System Updates
|
|
78
|
+
|
|
79
|
+
- **Enhanced Type Definitions** β Updated `IAnnouncement` and `IHero` interfaces
|
|
80
|
+
- Added `active_duration_seconds`, `last_activated_at`, and `last_deactivated_at` fields
|
|
81
|
+
|
|
82
|
+
### π Documentation
|
|
83
|
+
|
|
84
|
+
- **New Announcement Resolvers Documentation** β Added comprehensive guide for the new resolver system
|
|
85
|
+
|
|
86
|
+
## [2.1.8] - 2025-12-25
|
|
87
|
+
|
|
88
|
+
### β¨ Countdown Timer Styling Enhancements
|
|
89
|
+
|
|
90
|
+
- **Enhanced Countdown Timer Color Tokens** β Added comprehensive color customization for countdown timers
|
|
91
|
+
- `counterDigitColor` β Customize the color of countdown digits (default: `#FFFFFF`)
|
|
92
|
+
- `counterDigitBackgroundColor` β Customize the background color of countdown digits (default: `#000000`)
|
|
93
|
+
- `counterTextColor` β Customize the color of labels and separators (default: uses `counterDigitColor`)
|
|
94
|
+
- **Fixed Token Resolution** β Corrected `counterTextColor` token resolution to use proper fallback handling
|
|
95
|
+
- **Documentation Updates** β Added comprehensive countdown timer styling documentation to README
|
|
96
|
+
|
|
97
|
+
## [2.1.5] - 2025-12-05
|
|
98
|
+
|
|
99
|
+
### π§ Type System Updates
|
|
100
|
+
|
|
101
|
+
- **Enhanced Type Definitions** β Updated `IAnnouncement` and `IHero` interfaces to match Supabase schema
|
|
102
|
+
- Added `active_start_date: string | null` field
|
|
103
|
+
- Added `active_end_date: string | null` field
|
|
104
|
+
- Made `created_at` and `updated_at` nullable
|
|
105
|
+
- Made `page` field nullable in `IHero`
|
|
106
|
+
|
|
107
|
+
### ποΈ Breaking Changes
|
|
108
|
+
|
|
109
|
+
- **Removed Theme & Variant System** β Simplified styling system by removing unused theme/variant abstractions
|
|
110
|
+
- Removed `theme` property from `IFragmentStyling`
|
|
111
|
+
- Removed `variant` property from `IFragmentStyling`
|
|
112
|
+
- Focus now exclusively on design tokens, slots, responsive design, and state-based styling
|
|
113
|
+
- Updated all documentation to reflect the simplified system
|
|
114
|
+
|
|
115
|
+
## [2.1.4] - 2025-11-18
|
|
116
|
+
|
|
117
|
+
### β¨ UI/UX Improvements
|
|
118
|
+
|
|
119
|
+
- **Countdown Timer Enhancements** β Improved countdown timer visual design
|
|
120
|
+
- Removed background color from countdown digits for cleaner appearance
|
|
121
|
+
- Increased digit font size for better visibility
|
|
122
|
+
- Shortened label text: "Minutes" β "Mins", "Seconds" β "Secs"
|
|
123
|
+
|
|
124
|
+
- **Announcement Component Updates** β Enhanced announcement functionality
|
|
125
|
+
- Countdown timers can now display alongside buttons when both are enabled
|
|
126
|
+
- Improved layout flexibility for countdown announcements with CTAs
|
|
127
|
+
|
|
128
|
+
## [2.1.2] - 2025-10-30
|
|
129
|
+
|
|
130
|
+
### π Documentation Updates
|
|
131
|
+
|
|
132
|
+
- **Enhanced README** β Updated documentation to highlight new click tracking features
|
|
133
|
+
- **Feature Highlights** β Added comprehensive documentation for the enhanced click tracking system
|
|
134
|
+
- **Usage Examples** β Improved examples showing the separation of button destinations and tracking
|
|
135
|
+
|
|
136
|
+
## [2.1.1] - 2025-10-30
|
|
137
|
+
|
|
138
|
+
### π― Enhanced Click Tracking System
|
|
139
|
+
|
|
140
|
+
- **Improved Click Tracking Architecture** β Separated button destinations from click tracking for better user experience
|
|
141
|
+
- `buttonHref` now contains the actual destination URL (no redirect)
|
|
142
|
+
- `clickHref` contains the tracking URL for metrics collection
|
|
143
|
+
- Users go directly to intended destinations instead of through redirects
|
|
144
|
+
- **New `fireClickMetric()` Function** β Advanced click tracking without relying on redirects
|
|
145
|
+
- Uses `fetch()` with `no-cors` mode and `keepalive` for reliable tracking
|
|
146
|
+
- Falls back to Image pixel tracking for maximum compatibility
|
|
147
|
+
- Handles server-side rendering gracefully
|
|
148
|
+
- **Removed Automatic New Tab Behavior** β Links no longer force `target="_blank"`
|
|
149
|
+
- Provides more natural user experience
|
|
150
|
+
- Allows developers to control link behavior explicitly
|
|
151
|
+
- Maintains accessibility with proper ARIA labels
|
|
152
|
+
|
|
153
|
+
### π Technical Improvements
|
|
154
|
+
|
|
155
|
+
- **Enhanced Component Props** β Both Hero and Announcement components now accept separate tracking parameters
|
|
156
|
+
- `buttonHref` for the actual destination
|
|
157
|
+
- `clickHref` for tracking metrics
|
|
158
|
+
- **Better Error Handling** β Click tracking fails gracefully without affecting user experience
|
|
159
|
+
- **Performance Optimized** β Non-blocking click tracking that doesn't delay navigation
|
|
160
|
+
- **Cross-Browser Compatible** β Works across all modern browsers with appropriate fallbacks
|
|
161
|
+
|
|
162
|
+
### π Usage Examples
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// The SDK automatically handles the separation of concerns
|
|
166
|
+
const heroContent = {
|
|
167
|
+
title: "Shop Now",
|
|
168
|
+
buttonText: "Get Started",
|
|
169
|
+
buttonLink: "https://example.com/products", // Direct destination
|
|
170
|
+
clickUrlBase: "https://tracking.example.com/click", // Tracking base
|
|
171
|
+
// SDK automatically creates:
|
|
172
|
+
// - buttonHref: "https://example.com/products" (direct link)
|
|
173
|
+
// - clickHref: "https://tracking.example.com/click?u=..." (tracking)
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
<Hero content={heroContent} />;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### β‘ Performance Benefits
|
|
180
|
+
|
|
181
|
+
- **Faster Navigation** β Users go directly to destinations without redirect delays
|
|
182
|
+
- **Reliable Tracking** β Click metrics are captured even if users navigate away quickly
|
|
183
|
+
- **Better SEO** β Direct links improve search engine crawling and indexing
|
|
184
|
+
- **Enhanced UX** β More predictable link behavior for better user experience
|
|
185
|
+
|
|
186
|
+
### π Backward Compatibility
|
|
187
|
+
|
|
188
|
+
- **No Breaking Changes** β All existing implementations continue to work
|
|
189
|
+
- **Automatic Upgrade** β New tracking system activates automatically when `clickUrlBase` is present
|
|
190
|
+
- **Legacy Support** β Existing tracking URLs continue to function as before
|
|
191
|
+
|
|
192
|
+
## [2.1.0] - 2025-10-27
|
|
193
|
+
|
|
194
|
+
### π¨ Enhanced Hero Styling System
|
|
195
|
+
|
|
196
|
+
- **New Hero Resolvers Utility** β Comprehensive utility system for advanced Hero component customization
|
|
197
|
+
- `resolveHeroColors()` β Intelligent color resolution with fallback handling
|
|
198
|
+
- `resolveHeroTypography()` β Typography settings with font family, size, and line height control
|
|
199
|
+
- `resolveContentWidthClass()` β Dynamic content width management
|
|
200
|
+
- `resolvePosition()` β Content positioning (left, center, right alignment)
|
|
201
|
+
- `resolveHeight()` β Flexible height configuration
|
|
202
|
+
- `renderText()` β Unified text rendering with typography and styling support
|
|
203
|
+
|
|
204
|
+
### β¨ Advanced Typography Features
|
|
205
|
+
|
|
206
|
+
- **Font Family Support** β Built-in support for popular font families:
|
|
207
|
+
- Roboto, Open Sans, Lato, Montserrat, Poppins, Inter, Nunito Sans, Source Sans Pro
|
|
208
|
+
- Custom font family support through `FontKey` type system
|
|
209
|
+
- **Responsive Typography** β Granular control over font sizes and line heights
|
|
210
|
+
- Separate title and description typography settings
|
|
211
|
+
- Tailwind CSS class integration for responsive design
|
|
212
|
+
- **Typography Tokens** β New styling tokens for enhanced typography control:
|
|
213
|
+
- `titleFontSize`, `titleLineHeight`, `titleFont`
|
|
214
|
+
- `descriptionFontSize`, `descriptionLineHeight`, `descriptionFont`
|
|
215
|
+
|
|
216
|
+
### ποΈ Layout & Positioning Enhancements
|
|
217
|
+
|
|
218
|
+
- **Content Positioning** β New positioning system for Hero content alignment
|
|
219
|
+
- Left, center, and right alignment options
|
|
220
|
+
- Responsive positioning with proper text alignment
|
|
221
|
+
- **Content Width Control** β Dynamic content width management
|
|
222
|
+
- Configurable content container widths
|
|
223
|
+
- Responsive design integration
|
|
224
|
+
- **Height Management** β Flexible height configuration system
|
|
225
|
+
- Custom height classes support
|
|
226
|
+
- Default height fallbacks
|
|
227
|
+
|
|
228
|
+
### π― Developer Experience Improvements
|
|
229
|
+
|
|
230
|
+
- **Type Safety** β Enhanced TypeScript interfaces for all new features
|
|
231
|
+
- `HeroResolvedColors` interface for color resolution
|
|
232
|
+
- `HeroTypographySettings` interface for typography configuration
|
|
233
|
+
- `FontKey` type for font family validation
|
|
234
|
+
- **Utility Functions** β New helper functions for common operations
|
|
235
|
+
- `joinClassNames()` β Safe CSS class concatenation
|
|
236
|
+
- `fallbackColor()` β Color value validation with fallbacks
|
|
237
|
+
- **Better Defaults** β Comprehensive default values for all styling options
|
|
238
|
+
- `DEFAULT_COLORS` for color fallbacks
|
|
239
|
+
- `DEFAULT_TYPOGRAPHY` for typography defaults
|
|
240
|
+
- `FONT_FAMILY_MAP` for font family mappings
|
|
241
|
+
|
|
242
|
+
### π Backward Compatibility
|
|
243
|
+
|
|
244
|
+
- **Seamless Migration** β All existing Hero components continue to work without changes
|
|
245
|
+
- **Progressive Enhancement** β New features are opt-in and don't affect existing implementations
|
|
246
|
+
- **Legacy Support** β Existing styling approaches remain fully supported
|
|
247
|
+
|
|
248
|
+
### π Usage Examples
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// Enhanced Hero with new typography and positioning
|
|
252
|
+
const heroContent = {
|
|
253
|
+
title: "Welcome to Our Store",
|
|
254
|
+
description: "Discover amazing products",
|
|
255
|
+
buttonText: "Shop Now",
|
|
256
|
+
buttonLink: "/products",
|
|
257
|
+
imageUrl: "https://example.com/hero.jpg",
|
|
258
|
+
|
|
259
|
+
styling: {
|
|
260
|
+
tokens: {
|
|
261
|
+
colors: {
|
|
262
|
+
title: "#ffffff",
|
|
263
|
+
text: "#f0f0f0",
|
|
264
|
+
button: "#007bff",
|
|
265
|
+
buttonText: "#ffffff",
|
|
266
|
+
background: "#1a1a1a",
|
|
267
|
+
},
|
|
268
|
+
typography: {
|
|
269
|
+
titleFont: "montserrat",
|
|
270
|
+
titleFontSize: "text-6xl",
|
|
271
|
+
titleLineHeight: "leading-tight",
|
|
272
|
+
descriptionFont: "inter",
|
|
273
|
+
descriptionFontSize: "text-xl",
|
|
274
|
+
descriptionLineHeight: "leading-relaxed",
|
|
275
|
+
},
|
|
276
|
+
layout: {
|
|
277
|
+
contentWidth: "max-w-4xl",
|
|
278
|
+
position: "center",
|
|
279
|
+
height: "min-h-screen",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### π Technical Improvements
|
|
287
|
+
|
|
288
|
+
- **Performance Optimized** β Efficient color and typography resolution
|
|
289
|
+
- **Memory Efficient** β Optimized utility functions with minimal overhead
|
|
290
|
+
- **Tree Shakeable** β Individual utility functions can be imported separately
|
|
291
|
+
- **CSS-in-JS Ready** β Full compatibility with styled-components and emotion
|
|
292
|
+
|
|
293
|
+
## [1.0.6] - 2025-10-16
|
|
294
|
+
|
|
295
|
+
### π Next.js Caching Fix
|
|
296
|
+
|
|
297
|
+
- **Fixed Vercel/Next.js Caching Issues** β Resolved aggressive caching that prevented fresh data from appearing in production deployments
|
|
298
|
+
- Added `cache: 'no-store'` by default for all `fetchResource()` calls
|
|
299
|
+
- Added Next.js-specific `revalidate: 0` configuration
|
|
300
|
+
- Added cache-busting headers (`Cache-Control`, `Pragma`) to prevent CDN caching
|
|
301
|
+
- Smart environment detection for Next.js vs other frameworks
|
|
302
|
+
|
|
303
|
+
### β¨ New Cache Management Features
|
|
304
|
+
|
|
305
|
+
- **Cache Configuration Options** β Added optional `cacheOptions` parameter to `fetchResource()`
|
|
306
|
+
- `cache`: Control request cache mode (default: 'no-store' for fresh data)
|
|
307
|
+
- `revalidate`: Next.js revalidation time in seconds (default: 0)
|
|
308
|
+
- `tags`: Next.js cache tags for selective invalidation
|
|
309
|
+
- **Cache Invalidation Utilities** β New helper functions for cache management
|
|
310
|
+
- `revalidateFragmentCache()` β Invalidate all or specific Fragment caches
|
|
311
|
+
- `revalidateResourceType()` β Invalidate cache for specific resource type
|
|
312
|
+
- `revalidateAllFragmentCaches()` β Clear all Fragment-related caches
|
|
313
|
+
- `createCacheTag()` / `createCacheTags()` β Generate cache tags
|
|
314
|
+
|
|
315
|
+
### π Technical Improvements
|
|
316
|
+
|
|
317
|
+
- **Environment Detection** β Automatic Next.js environment detection for optimal cache settings
|
|
318
|
+
- **Backward Compatibility** β All existing code continues to work without changes
|
|
319
|
+
- **TypeScript Support** β Full type definitions for new cache options
|
|
320
|
+
|
|
321
|
+
### π Usage Examples
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// Default behavior - always fresh data (recommended)
|
|
325
|
+
const announcements = await fetchResource({
|
|
326
|
+
baseUrl: process.env.FRAGMENT_BASE_URL,
|
|
327
|
+
apiKey: process.env.FRAGMENT_API_KEY,
|
|
328
|
+
type: ResourceType.Announcements,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Optional: Enable caching for performance
|
|
332
|
+
const cachedHeroes = await fetchResource({
|
|
333
|
+
baseUrl: process.env.FRAGMENT_BASE_URL,
|
|
334
|
+
apiKey: process.env.FRAGMENT_API_KEY,
|
|
335
|
+
type: ResourceType.HeroBanners,
|
|
336
|
+
cacheOptions: {
|
|
337
|
+
cache: "default",
|
|
338
|
+
revalidate: 300, // 5 minutes
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Cache invalidation (server-side only)
|
|
343
|
+
import { revalidateResourceType } from "fragment-headless-sdk";
|
|
344
|
+
await revalidateResourceType(ResourceType.Announcements);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### β οΈ Migration Notes
|
|
348
|
+
|
|
349
|
+
- **No breaking changes** β Existing code works without modification
|
|
350
|
+
- **Fresh data by default** β Your database updates will now appear immediately in production
|
|
351
|
+
- **Opt-in caching** β Use `cacheOptions` if you want to enable caching for performance
|
|
352
|
+
|
|
353
|
+
## [1.0.5] - 2025-09-28
|
|
354
|
+
|
|
355
|
+
### β¨ New Features
|
|
356
|
+
|
|
357
|
+
- **Metrics Tracking** β Added built-in view and click tracking for both **Hero** and **Announcement** sections.
|
|
358
|
+
- Each resourceβs `content` object now includes two server-generated fields:
|
|
359
|
+
- `impressionUrl` β 1Γ1 pixel URL automatically fired when the component enters the viewport.
|
|
360
|
+
- `clickUrlBase` β base redirect URL used to record button clicks before sending the user to the final destination.
|
|
361
|
+
- The SDKβs `<Hero>` and `<Announcement>` components now automatically:
|
|
362
|
+
- trigger a view pixel when visible, and
|
|
363
|
+
- wrap their CTA buttons with a signed click-tracking redirect.
|
|
364
|
+
|
|
365
|
+
### π Technical Notes
|
|
366
|
+
|
|
367
|
+
- The `makeSignedMetricUrls` helper was refactored to attach `impressionUrl` and `clickUrlBase` **inside the `content` object** for each item returned by the API.
|
|
368
|
+
- New client-side utilities exported from `utils`:
|
|
369
|
+
- `buildClickUrl()` β safely appends the final destination (`&u=...`) to a signed `clickUrlBase`.
|
|
370
|
+
- `fireImpressionWhenVisible()` β fires a pixel only once when an element is at least 30 % visible.
|
|
371
|
+
|
|
372
|
+
### β οΈ Migration Notes
|
|
373
|
+
|
|
374
|
+
- **No breaking changes.**
|
|
375
|
+
Existing components continue to work; the new tracking is automatic when you upgrade to v1.0.5.
|
|
376
|
+
- If you build custom CTAs outside the provided components, use the new helpers to track clicks and views manually.
|
|
377
|
+
|
|
378
|
+
## [1.0.4] - 2025-09-27
|
|
379
|
+
|
|
380
|
+
- **Types:** `IHero` now includes `views_count: number` and `clicks_count: number`.
|
|
381
|
+
|
|
382
|
+
### π Notes
|
|
383
|
+
|
|
384
|
+
- No breaking changes..
|
|
385
|
+
|
|
386
|
+
## [1.0.3] - 2025-09-21
|
|
387
|
+
|
|
388
|
+
### π¨ UI/UX Improvements
|
|
389
|
+
|
|
390
|
+
- **Announcement Type Rename** - Changed `AnnouncementType.Announcement` to `AnnouncementType.Static` for better clarity
|
|
391
|
+
- **Countdown Timer Styling** - Removed white background from countdown timer for cleaner appearance
|
|
392
|
+
- **Layout Optimization** - Improved announcement banner layout with:
|
|
393
|
+
- Removed top/bottom padding (`py-3`) for more compact design
|
|
394
|
+
- Added 50px minimum height for consistent banner sizing
|
|
395
|
+
- Enhanced vertical centering of all content elements
|
|
396
|
+
- **Timer Digit Sizing** - Made countdown timer digits smaller and more compact:
|
|
397
|
+
- Reduced digit size from 24Γ28px to 20Γ24px
|
|
398
|
+
- Changed font size from `text-xl` to `text-base`
|
|
399
|
+
|
|
400
|
+
### π§ Technical Changes
|
|
401
|
+
|
|
402
|
+
- Updated `announcementTypes` array to reflect new "Static" label
|
|
403
|
+
- Improved flexbox layout for better vertical alignment
|
|
404
|
+
- Maintained responsive design across all screen sizes
|
|
405
|
+
|
|
406
|
+
## [1.0.2] - 2025-09-20
|
|
407
|
+
|
|
408
|
+
### π Breaking Changes
|
|
409
|
+
|
|
410
|
+
- **Component Naming** - Renamed `Banner` component and all related types to `Announcement`
|
|
411
|
+
- `Banner` β `Announcement`
|
|
412
|
+
- `IBannerContent` β `IAnnouncementContent`
|
|
413
|
+
- `IBanner` β `IAnnouncement`
|
|
414
|
+
- `BannerType` β `AnnouncementType`
|
|
415
|
+
- `BannerStatus` β `AnnouncementStatus`
|
|
416
|
+
- `BannerButton` β `AnnouncementButton`
|
|
417
|
+
- `BannerStyles` β `AnnouncementStyles`
|
|
418
|
+
- `bannerHtml` property β `announcementHtml`
|
|
419
|
+
|
|
420
|
+
- **Resource Type Updates** - Updated resource type enums for consistency
|
|
421
|
+
- `ResourceType.HeroSections` β `ResourceType.HeroBanners`
|
|
422
|
+
- `ResourceType.Banners` β `ResourceType.Announcements`
|
|
423
|
+
|
|
424
|
+
### π API Endpoint Changes
|
|
425
|
+
|
|
426
|
+
- Updated API endpoints to match new naming:
|
|
427
|
+
- `/api/v1/hero-sections` β `/api/v1/hero-banners`
|
|
428
|
+
- `/api/v1/banners` β `/api/v1/announcements`
|
|
429
|
+
|
|
430
|
+
### π Documentation
|
|
431
|
+
|
|
432
|
+
- Updated all documentation to reflect new component and type names
|
|
433
|
+
- Updated README.md examples with new ResourceType values
|
|
434
|
+
- Updated code examples throughout
|
|
435
|
+
|
|
436
|
+
### π οΈ Migration Guide
|
|
437
|
+
|
|
438
|
+
To update your existing code:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// Before (v1.0.1)
|
|
442
|
+
import {
|
|
443
|
+
Banner,
|
|
444
|
+
BannerType,
|
|
445
|
+
IBannerContent,
|
|
446
|
+
ResourceType,
|
|
447
|
+
} from "fragment-headless-sdk";
|
|
448
|
+
|
|
449
|
+
const banners = await fetchResource({
|
|
450
|
+
type: ResourceType.Banners,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
<Banner content={bannerContent} type={BannerType.Standard} />;
|
|
454
|
+
|
|
455
|
+
// After (v1.0.2)
|
|
456
|
+
import {
|
|
457
|
+
Announcement,
|
|
458
|
+
AnnouncementType,
|
|
459
|
+
IAnnouncementContent,
|
|
460
|
+
ResourceType,
|
|
461
|
+
} from "fragment-headless-sdk";
|
|
462
|
+
|
|
463
|
+
const announcements = await fetchResource({
|
|
464
|
+
type: ResourceType.Announcements,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
<Announcement
|
|
468
|
+
content={announcementContent}
|
|
469
|
+
type={AnnouncementType.Announcement}
|
|
470
|
+
/>;
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## [1.0.1] - 2025-09-07
|
|
474
|
+
|
|
475
|
+
### π Initial Release
|
|
476
|
+
|
|
477
|
+
The official SDK for integrating with fragment-shopify CMS. Production-ready with full API key authentication support.
|
|
478
|
+
|
|
479
|
+
### Features
|
|
480
|
+
|
|
481
|
+
- **Complete TypeScript Support** - Full type definitions for all components and API responses
|
|
482
|
+
- **React Components** - Pre-built Hero and Announcement components with responsive design
|
|
483
|
+
- **API Integration** - Built-in utilities for fetching sections from fragment-shopify app
|
|
484
|
+
- **Production Ready** - Full API key authentication with v1 endpoints
|
|
485
|
+
- **Tailwind CSS** - Styled components with customizable design system
|
|
486
|
+
|
|
487
|
+
### Components
|
|
488
|
+
|
|
489
|
+
- **Hero Component** - Responsive hero sections with desktop/mobile layouts
|
|
490
|
+
- Support for images, videos, and call-to-action buttons
|
|
491
|
+
- Customizable content and styling
|
|
492
|
+
- **Announcement Component** - Flexible announcement bars with multiple display types
|
|
493
|
+
- Standard, marquee, and countdown announcement types
|
|
494
|
+
- `AnnouncementButton` and `CountdownTimer` sub-components
|
|
495
|
+
|
|
496
|
+
### API Integration
|
|
497
|
+
|
|
498
|
+
- **`fetchResource()` Function** - Simple API for fetching sections
|
|
499
|
+
- **API Key Authentication** - Secure authentication using `keyId:secret` format
|
|
500
|
+
- **v1 Endpoints** - Production endpoints (`/api/v1/announcements`, `/api/v1/hero-banners`)
|
|
501
|
+
- **Error Handling** - Comprehensive error handling and logging
|
|
502
|
+
- **Type Safety** - Full TypeScript support for all API responses
|
|
503
|
+
|
|
504
|
+
### Usage
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import {
|
|
508
|
+
fetchResource,
|
|
509
|
+
ResourceType,
|
|
510
|
+
Hero,
|
|
511
|
+
Announcement,
|
|
512
|
+
} from "fragment-headless-sdk";
|
|
513
|
+
|
|
514
|
+
// Fetch data
|
|
515
|
+
const heroes = await fetchResource({
|
|
516
|
+
baseUrl: process.env.EXTERNAL_API_URL,
|
|
517
|
+
apiKey: process.env.FRAGMENT_API_KEY,
|
|
518
|
+
type: ResourceType.HeroBanners,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Render components
|
|
522
|
+
<Hero content={heroes[0]?.content} />;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Environment Variables
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
EXTERNAL_API_URL=https://your-fragment-app.vercel.app
|
|
529
|
+
FRAGMENT_API_KEY=bh_a1b2c3d4e5f6:your-64-char-secret
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Upcoming Changes
|
|
535
|
+
|
|
536
|
+
### v1.1.0 (Planned)
|
|
537
|
+
|
|
538
|
+
- Enhanced error handling with specific error types
|
|
539
|
+
- Support for additional section types
|
|
540
|
+
- Caching and performance optimizations
|
|
541
|
+
|
|
542
|
+
### v1.2.0 (Planned)
|
|
543
|
+
|
|
544
|
+
- Real-time updates via webhooks
|
|
545
|
+
- Advanced filtering and sorting options
|
|
546
|
+
- Batch operations support
|
|
547
|
+
- TypeScript strict mode compatibility
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Support
|
|
552
|
+
|
|
553
|
+
- **Documentation**: [README.md](./README.md)
|
|
554
|
+
- **NPM Package**: https://www.npmjs.com/package/fragment-shopify-sdk
|
|
555
|
+
- **Issues**: Please report issues in the GitHub repository
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fragment-headless-sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
|
+
"description": "Headless SDK for Fragment Shopify storefronts",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -12,14 +13,37 @@
|
|
|
12
13
|
"./styles": "./dist/styles/fragment-sdk.css"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"docs/CHANGELOG.md"
|
|
16
18
|
],
|
|
19
|
+
"sideEffects": false,
|
|
17
20
|
"scripts": {
|
|
18
|
-
"build": "tsc"
|
|
21
|
+
"build": "tsc && mkdir -p dist/styles && cp src/styles/*.css dist/styles/",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"type-check": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist .turbo"
|
|
19
25
|
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/sevenbrand/fragment-monorepo",
|
|
33
|
+
"directory": "packages/sdk"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"shopify",
|
|
37
|
+
"headless",
|
|
38
|
+
"fragment",
|
|
39
|
+
"sdk",
|
|
40
|
+
"storefront"
|
|
41
|
+
],
|
|
20
42
|
"dependencies": {
|
|
21
|
-
"@heroicons/react": "^2.2.0"
|
|
22
|
-
|
|
43
|
+
"@heroicons/react": "^2.2.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
23
47
|
},
|
|
24
48
|
"devDependencies": {
|
|
25
49
|
"@types/node": "^24.7.2",
|
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.3.
|
|
7
|
+
**v2.3.1** - Enhanced attribution tracking with session storage and global API
|
|
8
8
|
|
|
9
9
|
> See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
|
|
10
10
|
|
|
@@ -73,21 +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
|
|
76
|
+
- π **Automatic Event Tracking**: Automatic `section_view` and `section_click` events
|
|
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
82
|
|
|
84
|
-
### Attribution Tracking System (v2.
|
|
83
|
+
### Attribution Tracking System (v2.4.0+)
|
|
85
84
|
|
|
86
|
-
- π― **
|
|
87
|
-
- πΎ **SessionStorage Integration**:
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
85
|
+
- π― **Session-Based Attribution**: Tracks which section was clicked for purchase/add-to-cart attribution
|
|
86
|
+
- πΎ **SessionStorage Integration**: Uses sessionStorage for session-scoped attribution (last-click wins)
|
|
87
|
+
- π **Global API Access**: Exposed as `window.fragmentAttribution` for Shopify theme integrations
|
|
88
|
+
- π **Type-Safe**: Full TypeScript support with proper type definitions
|
|
89
|
+
- β
**Data Validation**: Automatic validation and cleanup of corrupted attribution data
|
|
90
|
+
- π§Ή **Auto-Cleanup**: Invalid or corrupted data is automatically cleared from storage
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
@@ -126,7 +126,7 @@ module.exports = {
|
|
|
126
126
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
127
127
|
path.join(
|
|
128
128
|
__dirname,
|
|
129
|
-
"node_modules/fragment-headless-sdk/dist/**/*.{js,ts,jsx,tsx}"
|
|
129
|
+
"node_modules/fragment-headless-sdk/dist/**/*.{js,ts,jsx,tsx}",
|
|
130
130
|
),
|
|
131
131
|
],
|
|
132
132
|
theme: {
|
|
@@ -377,8 +377,9 @@ interface IHeroContent {
|
|
|
377
377
|
imageUrl: string;
|
|
378
378
|
mobileImageUrl: string;
|
|
379
379
|
videoUrl?: string;
|
|
380
|
-
|
|
381
|
-
|
|
380
|
+
measurementId?: string; // GA4 measurement ID (from app config)
|
|
381
|
+
sectionId?: string; // Section ID for GA4 event params
|
|
382
|
+
sectionType?: SectionType;
|
|
382
383
|
styling?: IHeroStyling; // v2.0+ enhanced styling
|
|
383
384
|
}
|
|
384
385
|
```
|
|
@@ -434,8 +435,9 @@ interface IAnnouncementContent {
|
|
|
434
435
|
buttonLink: string;
|
|
435
436
|
announcementHtml: string;
|
|
436
437
|
counterEndDate?: string;
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
measurementId?: string; // GA4 measurement ID (from app config)
|
|
439
|
+
sectionId?: string; // Section ID for GA4 event params
|
|
440
|
+
sectionType?: SectionType;
|
|
439
441
|
styling?: IAnnouncementStyling; // v2.0+ enhanced styling
|
|
440
442
|
}
|
|
441
443
|
```
|
|
@@ -490,6 +492,105 @@ const announcementContent = {
|
|
|
490
492
|
};
|
|
491
493
|
```
|
|
492
494
|
|
|
495
|
+
## π― Attribution Tracking
|
|
496
|
+
|
|
497
|
+
The SDK automatically tracks which section was clicked for attribution purposes. This is useful for tracking conversions (purchases, add-to-cart) back to the original section interaction.
|
|
498
|
+
|
|
499
|
+
### Automatic Attribution
|
|
500
|
+
|
|
501
|
+
When a user clicks a button in a Hero or Announcement section, the SDK automatically stores attribution data in `sessionStorage`:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// Attribution is automatically set when clicks occur
|
|
505
|
+
// No additional code needed - it happens automatically!
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Global API (Shopify Theme Integration)
|
|
509
|
+
|
|
510
|
+
For Shopify theme integrations, attribution data is exposed globally via `window.fragmentAttribution`:
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
// Get current attribution data
|
|
514
|
+
const attribution = window.fragmentAttribution.get();
|
|
515
|
+
// Returns: { sectionId: "uuid", sectionType: "announcement" | "hero_banner", timestamp: 1234567890 }
|
|
516
|
+
// Or: null if no attribution exists
|
|
517
|
+
|
|
518
|
+
// Manually set attribution (usually not needed - SDK handles this automatically)
|
|
519
|
+
window.fragmentAttribution.set("section-uuid", "announcement");
|
|
520
|
+
|
|
521
|
+
// Clear attribution (e.g., after successful conversion tracking)
|
|
522
|
+
window.fragmentAttribution.clear();
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Programmatic Access
|
|
526
|
+
|
|
527
|
+
You can also import and use attribution functions directly:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import { getAttribution, clearAttribution } from "fragment-headless-sdk";
|
|
531
|
+
|
|
532
|
+
// Get attribution data
|
|
533
|
+
const attribution = getAttribution();
|
|
534
|
+
if (attribution) {
|
|
535
|
+
console.log(`User clicked section: ${attribution.sectionId}`);
|
|
536
|
+
console.log(`Section type: ${attribution.sectionType}`);
|
|
537
|
+
console.log(`Clicked at: ${new Date(attribution.timestamp)}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Clear attribution after tracking conversion
|
|
541
|
+
clearAttribution();
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Use Cases
|
|
545
|
+
|
|
546
|
+
**Purchase Attribution:**
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
// In your Shopify checkout success page
|
|
550
|
+
if (window.fragmentAttribution) {
|
|
551
|
+
const attribution = window.fragmentAttribution.get();
|
|
552
|
+
if (attribution) {
|
|
553
|
+
// Track purchase back to the section click
|
|
554
|
+
// e.g., send to analytics, update database, etc.
|
|
555
|
+
trackPurchase(attribution.sectionId, attribution.sectionType);
|
|
556
|
+
|
|
557
|
+
// Clear attribution after tracking
|
|
558
|
+
window.fragmentAttribution.clear();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Add-to-Cart Attribution:**
|
|
564
|
+
|
|
565
|
+
```javascript
|
|
566
|
+
// In your add-to-cart handler
|
|
567
|
+
if (window.fragmentAttribution) {
|
|
568
|
+
const attribution = window.fragmentAttribution.get();
|
|
569
|
+
if (attribution) {
|
|
570
|
+
// Track add-to-cart back to the section click
|
|
571
|
+
trackAddToCart(attribution.sectionId, attribution.sectionType);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Attribution Data Structure
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
interface FragmentAttribution {
|
|
580
|
+
sectionId: string; // UUID of the section that was clicked
|
|
581
|
+
sectionType: SectionType; // "announcement" | "hero_banner"
|
|
582
|
+
timestamp: number; // Unix timestamp of when the click occurred
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Features
|
|
587
|
+
|
|
588
|
+
- β
**Session-Scoped**: Attribution data persists for the browser session only
|
|
589
|
+
- β
**Last-Click Wins**: New clicks overwrite previous attribution data
|
|
590
|
+
- β
**Type-Safe**: Full TypeScript support with proper type definitions
|
|
591
|
+
- β
**Data Validation**: Automatically validates and cleans corrupted data
|
|
592
|
+
- β
**Graceful Degradation**: Works even if sessionStorage is unavailable
|
|
593
|
+
|
|
493
594
|
## π API Key Setup
|
|
494
595
|
|
|
495
596
|
Before using the SDK, you need to generate an API key from your Fragment-Shopify app:
|