@jwiedeman/gtm-kit-react 1.2.0 → 1.2.1
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/README.md +35 -3
- package/dist/index.cjs +10 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -191,13 +191,32 @@ if (hasProvider) {
|
|
|
191
191
|
|
|
192
192
|
### Setting Consent Defaults Before GTM Loads
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
**Recommended:** Use the `onBeforeInit` callback to set consent defaults before GTM loads:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { GtmProvider } from '@jwiedeman/gtm-kit-react';
|
|
198
|
+
import { consentPresets } from '@jwiedeman/gtm-kit';
|
|
199
|
+
|
|
200
|
+
function App() {
|
|
201
|
+
return (
|
|
202
|
+
<GtmProvider
|
|
203
|
+
config={{ containers: 'GTM-XXXXXX' }}
|
|
204
|
+
onBeforeInit={(client) => {
|
|
205
|
+
client.setConsentDefaults(consentPresets.eeaDefault, { region: ['EEA'] });
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<YourApp />
|
|
209
|
+
</GtmProvider>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Alternative:** Use `useGtmConsent` in a child component (child effects run before parent effects in React):
|
|
195
215
|
|
|
196
216
|
```tsx
|
|
197
217
|
import { GtmProvider, useGtmConsent } from '@jwiedeman/gtm-kit-react';
|
|
198
218
|
import { consentPresets } from '@jwiedeman/gtm-kit';
|
|
199
219
|
|
|
200
|
-
// Component that sets consent defaults on mount
|
|
201
220
|
function ConsentInitializer({ children }) {
|
|
202
221
|
const { setConsentDefaults } = useGtmConsent();
|
|
203
222
|
|
|
@@ -208,7 +227,6 @@ function ConsentInitializer({ children }) {
|
|
|
208
227
|
return <>{children}</>;
|
|
209
228
|
}
|
|
210
229
|
|
|
211
|
-
// App wrapper
|
|
212
230
|
function App() {
|
|
213
231
|
return (
|
|
214
232
|
<GtmProvider config={{ containers: 'GTM-XXXXXX' }}>
|
|
@@ -277,11 +295,25 @@ updateConsent({ ad_storage: 'granted', ad_user_data: 'granted' });
|
|
|
277
295
|
host: 'https://custom.host.com', // Optional
|
|
278
296
|
scriptAttributes: { nonce: '...' } // Optional: CSP
|
|
279
297
|
}}
|
|
298
|
+
onBeforeInit={(client) => {
|
|
299
|
+
// Called before GTM loads - ideal for consent defaults
|
|
300
|
+
client.setConsentDefaults(consentPresets.eeaDefault, { region: ['EEA'] });
|
|
301
|
+
}}
|
|
302
|
+
onAfterInit={(client) => {
|
|
303
|
+
// Called after GTM loads
|
|
304
|
+
console.log('GTM initialized');
|
|
305
|
+
}}
|
|
280
306
|
>
|
|
281
307
|
{children}
|
|
282
308
|
</GtmProvider>
|
|
283
309
|
```
|
|
284
310
|
|
|
311
|
+
| Prop | Type | Required | Description |
|
|
312
|
+
| ---------------- | --------------------------- | -------- | ------------------------------------------------- |
|
|
313
|
+
| `config` | `CreateGtmClientOptions` | Yes | GTM client configuration (containers, host, etc.) |
|
|
314
|
+
| `onBeforeInit` | `(client: GtmClient) => void` | No | Called before GTM initialization — use for consent defaults |
|
|
315
|
+
| `onAfterInit` | `(client: GtmClient) => void` | No | Called after GTM initialization |
|
|
316
|
+
|
|
285
317
|
---
|
|
286
318
|
|
|
287
319
|
## React Router Integration
|
package/dist/index.cjs
CHANGED
|
@@ -80,7 +80,7 @@ var useStableClient = (config) => {
|
|
|
80
80
|
}
|
|
81
81
|
return clientRef.current;
|
|
82
82
|
};
|
|
83
|
-
var GtmProvider = ({ config, children }) => {
|
|
83
|
+
var GtmProvider = ({ config, children, onBeforeInit, onAfterInit }) => {
|
|
84
84
|
const existingContext = react.useContext(GtmContext);
|
|
85
85
|
react.useEffect(() => {
|
|
86
86
|
if (existingContext) {
|
|
@@ -90,17 +90,23 @@ var GtmProvider = ({ config, children }) => {
|
|
|
90
90
|
if (existingContext) {
|
|
91
91
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
92
92
|
}
|
|
93
|
-
return /* @__PURE__ */ jsxRuntime.jsx(GtmProviderInner, { config, children });
|
|
93
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GtmProviderInner, { config, onBeforeInit, onAfterInit, children });
|
|
94
94
|
};
|
|
95
|
-
var GtmProviderInner = ({ config, children }) => {
|
|
95
|
+
var GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }) => {
|
|
96
96
|
const client = useStableClient(config);
|
|
97
97
|
react.useEffect(() => {
|
|
98
98
|
warnOnOrphanedSsrScripts(extractContainerIds(config));
|
|
99
|
+
if (onBeforeInit) {
|
|
100
|
+
onBeforeInit(client);
|
|
101
|
+
}
|
|
99
102
|
client.init();
|
|
103
|
+
if (onAfterInit) {
|
|
104
|
+
onAfterInit(client);
|
|
105
|
+
}
|
|
100
106
|
return () => {
|
|
101
107
|
client.teardown();
|
|
102
108
|
};
|
|
103
|
-
}, [client, config]);
|
|
109
|
+
}, [client, config, onBeforeInit, onAfterInit]);
|
|
104
110
|
const value = react.useMemo(
|
|
105
111
|
() => ({
|
|
106
112
|
client,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return <GtmProviderInner config={config}>{children}</GtmProviderInner>;\n};\n\nconst GtmProviderInner = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n client.init();\n return () => {\n client.teardown();\n };\n }, [client, config]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n\n /**\n * Callback executed before GTM initialization.\n * Use this to set consent defaults.\n *\n * @example\n * ```tsx\n * <GtmProvider\n * config={{ containers: 'GTM-XXXXXX' }}\n * onBeforeInit={(client) => {\n * client.setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * }}\n * >\n * ```\n */\n onBeforeInit?: (client: GtmClient) => void;\n\n /**\n * Callback executed after GTM initialization.\n */\n onAfterInit?: (client: GtmClient) => void;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return (\n <GtmProviderInner config={config} onBeforeInit={onBeforeInit} onAfterInit={onAfterInit}>\n {children}\n </GtmProviderInner>\n );\n};\n\nconst GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n // Call onBeforeInit hook for consent defaults\n if (onBeforeInit) {\n onBeforeInit(client);\n }\n\n client.init();\n\n // Call onAfterInit hook\n if (onAfterInit) {\n onAfterInit(client);\n }\n\n return () => {\n client.teardown();\n };\n }, [client, config, onBeforeInit, onAfterInit]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -21,6 +21,28 @@ declare const isSsr: () => boolean;
|
|
|
21
21
|
declare const useHydrated: () => boolean;
|
|
22
22
|
interface GtmProviderProps extends PropsWithChildren {
|
|
23
23
|
config: CreateGtmClientOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Callback executed before GTM initialization.
|
|
26
|
+
* Use this to set consent defaults.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <GtmProvider
|
|
31
|
+
* config={{ containers: 'GTM-XXXXXX' }}
|
|
32
|
+
* onBeforeInit={(client) => {
|
|
33
|
+
* client.setConsentDefaults({
|
|
34
|
+
* ad_storage: 'denied',
|
|
35
|
+
* analytics_storage: 'denied'
|
|
36
|
+
* });
|
|
37
|
+
* }}
|
|
38
|
+
* >
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
onBeforeInit?: (client: GtmClient) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Callback executed after GTM initialization.
|
|
44
|
+
*/
|
|
45
|
+
onAfterInit?: (client: GtmClient) => void;
|
|
24
46
|
}
|
|
25
47
|
interface GtmContextValue {
|
|
26
48
|
client: GtmClient;
|
|
@@ -72,7 +94,7 @@ interface GtmConsentApi {
|
|
|
72
94
|
* return isGtmReady ? <TrackedContent /> : <LoadingContent />;
|
|
73
95
|
* ```
|
|
74
96
|
*/
|
|
75
|
-
declare const GtmProvider: ({ config, children }: GtmProviderProps) => JSX.Element;
|
|
97
|
+
declare const GtmProvider: ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps) => JSX.Element;
|
|
76
98
|
/**
|
|
77
99
|
* Hook to access the full GTM context. Throws if used outside GtmProvider.
|
|
78
100
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,28 @@ declare const isSsr: () => boolean;
|
|
|
21
21
|
declare const useHydrated: () => boolean;
|
|
22
22
|
interface GtmProviderProps extends PropsWithChildren {
|
|
23
23
|
config: CreateGtmClientOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Callback executed before GTM initialization.
|
|
26
|
+
* Use this to set consent defaults.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <GtmProvider
|
|
31
|
+
* config={{ containers: 'GTM-XXXXXX' }}
|
|
32
|
+
* onBeforeInit={(client) => {
|
|
33
|
+
* client.setConsentDefaults({
|
|
34
|
+
* ad_storage: 'denied',
|
|
35
|
+
* analytics_storage: 'denied'
|
|
36
|
+
* });
|
|
37
|
+
* }}
|
|
38
|
+
* >
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
onBeforeInit?: (client: GtmClient) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Callback executed after GTM initialization.
|
|
44
|
+
*/
|
|
45
|
+
onAfterInit?: (client: GtmClient) => void;
|
|
24
46
|
}
|
|
25
47
|
interface GtmContextValue {
|
|
26
48
|
client: GtmClient;
|
|
@@ -72,7 +94,7 @@ interface GtmConsentApi {
|
|
|
72
94
|
* return isGtmReady ? <TrackedContent /> : <LoadingContent />;
|
|
73
95
|
* ```
|
|
74
96
|
*/
|
|
75
|
-
declare const GtmProvider: ({ config, children }: GtmProviderProps) => JSX.Element;
|
|
97
|
+
declare const GtmProvider: ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps) => JSX.Element;
|
|
76
98
|
/**
|
|
77
99
|
* Hook to access the full GTM context. Throws if used outside GtmProvider.
|
|
78
100
|
*
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var useStableClient = (config) => {
|
|
|
78
78
|
}
|
|
79
79
|
return clientRef.current;
|
|
80
80
|
};
|
|
81
|
-
var GtmProvider = ({ config, children }) => {
|
|
81
|
+
var GtmProvider = ({ config, children, onBeforeInit, onAfterInit }) => {
|
|
82
82
|
const existingContext = useContext(GtmContext);
|
|
83
83
|
useEffect(() => {
|
|
84
84
|
if (existingContext) {
|
|
@@ -88,17 +88,23 @@ var GtmProvider = ({ config, children }) => {
|
|
|
88
88
|
if (existingContext) {
|
|
89
89
|
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
90
90
|
}
|
|
91
|
-
return /* @__PURE__ */ jsx(GtmProviderInner, { config, children });
|
|
91
|
+
return /* @__PURE__ */ jsx(GtmProviderInner, { config, onBeforeInit, onAfterInit, children });
|
|
92
92
|
};
|
|
93
|
-
var GtmProviderInner = ({ config, children }) => {
|
|
93
|
+
var GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }) => {
|
|
94
94
|
const client = useStableClient(config);
|
|
95
95
|
useEffect(() => {
|
|
96
96
|
warnOnOrphanedSsrScripts(extractContainerIds(config));
|
|
97
|
+
if (onBeforeInit) {
|
|
98
|
+
onBeforeInit(client);
|
|
99
|
+
}
|
|
97
100
|
client.init();
|
|
101
|
+
if (onAfterInit) {
|
|
102
|
+
onAfterInit(client);
|
|
103
|
+
}
|
|
98
104
|
return () => {
|
|
99
105
|
client.teardown();
|
|
100
106
|
};
|
|
101
|
-
}, [client, config]);
|
|
107
|
+
}, [client, config, onBeforeInit, onAfterInit]);
|
|
102
108
|
const value = useMemo(
|
|
103
109
|
() => ({
|
|
104
110
|
client,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AACP;AAAA,EACE;AAAA,OAOK;AA+LI;AAzLJ,IAAM,QAAQ,MAAe,OAAO,WAAW;AAc/C,IAAM,cAAc,MAAe;AAExC,SAAO;AAAA;AAAA;AAAA,IAGL,MAAM,MAAM;AAAA,IAAC;AAAA;AAAA,IAEb,MAAM;AAAA;AAAA,IAEN,MAAM;AAAA,EACR;AACF;AAsBA,IAAM,aAAa,cAAsC,IAAI;AAE7D,IAAM,uBAAuB,MAAY;AACvC,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AACF;AAEA,IAAM,qBAAqB,CAAC,eAAuC,eAA6C;AAC9G,MAAI,QAAQ,IAAI,aAAa,gBAAgB,kBAAkB,YAAY;AACzE,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AACF;AAKA,IAAM,sBAAsB,CAAC,WAA6C;AACxE,QAAM,aAAa,OAAO;AAC1B,QAAM,iBAAiB,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;AAE3E,SAAO,eAAe,IAAI,CAAC,cAAc;AACvC,QAAI,OAAO,cAAc,UAAU;AACjC,aAAO;AAAA,IACT;AACA,WAAO,UAAU;AAAA,EACnB,CAAC;AACH;AAOA,IAAM,2BAA2B,CAAC,yBAAyC;AACzE,MAAI,QAAQ,IAAI,aAAa,gBAAgB,OAAO,aAAa,aAAa;AAE5E,UAAM,aAAa,SAAS,iBAAiB,4CAA4C;AAEzF,eAAW,QAAQ,CAAC,WAAW;AAC7B,YAAM,MAAO,OAA6B;AAC1C,YAAM,UAAU,IAAI,MAAM,gBAAgB;AAC1C,YAAM,oBAAoB,mCAAU;AAEpC,UAAI,qBAAqB,CAAC,qBAAqB,SAAS,iBAAiB,GAAG;AAC1E,gBAAQ;AAAA,UACN,gEAAgE,iBAAiB,kKAEpC,iBAAiB;AAAA,QAChE;AAAA,MACF;AAAA,IACF,CAAC;AAGD,UAAM,eAAe,SAAS,iBAAiB,sDAAsD;AACrG,iBAAa,QAAQ,CAAC,WAAW;AAC/B,YAAM,MAAO,OAA6B;AAC1C,YAAM,UAAU,IAAI,MAAM,gBAAgB;AAC1C,YAAM,oBAAoB,mCAAU;AAEpC,UAAI,qBAAqB,CAAC,qBAAqB,SAAS,iBAAiB,GAAG;AAC1E,gBAAQ;AAAA,UACN,yEAAyE,iBAAiB;AAAA,QAE5F;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,IAAM,kBAAkB,CAAC,WAA8C;AACrE,QAAM,YAAY,OAAkB;AACpC,QAAM,YAAY,OAA+B;AAEjD,MAAI,CAAC,UAAU,SAAS;AACtB,cAAU,UAAU,gBAAgB,MAAM;AAC1C,cAAU,UAAU;AAAA,EACtB,WAAW,UAAU,SAAS;AAC5B,uBAAmB,UAAU,SAAS,MAAM;AAAA,EAC9C;AAEA,SAAO,UAAU;AACnB;AAsCO,IAAM,cAAc,CAAC,EAAE,QAAQ,SAAS,MAAqC;AAClF,QAAM,kBAAkB,WAAW,UAAU;AAG7C,YAAU,MAAM;AACd,QAAI,iBAAiB;AACnB,2BAAqB;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAGpB,MAAI,iBAAiB;AACnB,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,SAAO,oBAAC,oBAAiB,QAAiB,UAAS;AACrD;AAEA,IAAM,mBAAmB,CAAC,EAAE,QAAQ,SAAS,MAAqC;AAChF,QAAM,SAAS,gBAAgB,MAAM;AAErC,YAAU,MAAM;AAEd,6BAAyB,oBAAoB,MAAM,CAAC;AAEpD,WAAO,KAAK;AACZ,WAAO,MAAM;AACX,aAAO,SAAS;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,CAAC;AAEnB,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA,MAAM,CAACA,WAAU,OAAO,KAAKA,MAAK;AAAA,MAClC,oBAAoB,CAAC,OAAO,YAAY,OAAO,mBAAmB,OAAO,OAAO;AAAA,MAChF,eAAe,CAAC,OAAO,YAAY,OAAO,cAAc,OAAO,OAAO;AAAA,MACtE,SAAS,MAAM,OAAO,QAAQ;AAAA,MAC9B,WAAW,MAAM,OAAO,UAAU;AAAA,MAClC,SAAS,CAAC,aAAa,OAAO,QAAQ,QAAQ;AAAA,IAChD;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,oBAAC,WAAW,UAAX,EAAoB,OAAe,UAAS;AACtD;AAEA,IAAM,gBAAgB,MAAuB;AAC3C,QAAM,UAAU,WAAW,UAAU;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO;AACT;AAiBO,IAAM,SAAS;AAqBf,IAAM,0BAA0B,MAAe;AACpD,QAAM,UAAU,WAAW,UAAU;AACrC,SAAO,YAAY;AACrB;AAgBO,IAAM,eAAe,MAAiB;AAC3C,SAAO,cAAc,EAAE;AACzB;AAwBO,IAAM,aAAa,MAAyC;AACjE,SAAO,cAAc,EAAE;AACzB;AAoBO,IAAM,gBAAgB,MAAqB;AAChD,QAAM,EAAE,oBAAoB,cAAc,IAAI,cAAc;AAC5D,SAAO,QAAQ,OAAO,EAAE,oBAAoB,cAAc,IAAI,CAAC,oBAAoB,aAAa,CAAC;AACnG;AA6BO,IAAM,cAAc,MAA0C;AACnE,QAAM,EAAE,UAAU,IAAI,cAAc;AACpC,SAAO;AACT;AA6BO,IAAM,gBAAgB,MAAuB;AAClD,QAAM,EAAE,QAAQ,IAAI,cAAc;AAClC,SAAO;AACT;AA4BO,IAAM,oBAAoB,MAAe;AAC9C,QAAM,EAAE,SAAS,QAAQ,IAAI,cAAc;AAC3C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,QAAQ,CAAC;AAE9D,YAAU,MAAM;AAEd,QAAI,QAAQ,GAAG;AACb,qBAAe,IAAI;AACnB;AAAA,IACF;AAGA,UAAM,cAAc,QAAQ,MAAM;AAChC,qBAAe,IAAI;AAAA,IACrB,CAAC;AAED,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,OAAO,CAAC;AAErB,SAAO;AACT;AA4BO,IAAM,cAAc,MAAqB;AAC9C,QAAM,EAAE,QAAQ,IAAI,cAAc;AAClC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAwB;AAAA,IAC1D,UAAU;AAAA,IACV,eAAe,CAAC;AAAA,IAChB,cAAc;AAAA,EAChB,CAAC;AAED,YAAU,MAAM;AACd,UAAM,cAAc,QAAQ,CAAC,WAAW;AAlgB5C;AAmgBM,YAAM,gBAAgB,OAAO,OAAO,CAAC,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,SAAS;AAE1F,UAAI,cAAc,SAAS,GAAG;AAC5B,cAAM,cAAa,yBAAc,KAAK,CAAC,MAAM,EAAE,KAAK,MAAjC,mBAAoC,UAApC,YAA6C;AAChE,sBAAc;AAAA,UACZ,UAAU;AAAA,UACV;AAAA,UACA,cAAc;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC;AAEZ,SAAO;AACT;AAyBO,IAAM,mBAAN,cAA+B,UAAwD;AAAA,EAC5F,YAAY,OAA8B;AACxC,UAAM,KAAK;AAyBb,iBAAQ,MAAY;AAClB,WAAK,SAAS,EAAE,UAAU,OAAO,OAAO,KAAK,CAAC;AAAA,IAChD;AA1BE,SAAK,QAAQ,EAAE,UAAU,OAAO,OAAO,KAAK;AAAA,EAC9C;AAAA,EAEA,OAAO,yBAAyB,OAAqC;AACnE,WAAO,EAAE,UAAU,MAAM,MAAM;AAAA,EACjC;AAAA,EAEA,kBAAkB,OAAc,WAA4B;AAC1D,UAAM,EAAE,SAAS,YAAY,QAAQ,IAAI,aAAa,aAAa,IAAI,KAAK;AAE5E,QAAI,WAAW;AACb,cAAQ,MAAM,qDAAqD,KAAK;AACxE,cAAQ,MAAM,oCAAoC,UAAU,cAAc;AAAA,IAC5E;AAEA,QAAI,SAAS;AACX,UAAI;AACF,gBAAQ,OAAO,SAAS;AAAA,MAC1B,SAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAMA,SAAoB;AAClB,UAAM,EAAE,UAAU,MAAM,IAAI,KAAK;AACjC,UAAM,EAAE,UAAU,SAAS,IAAI,KAAK;AAEpC,QAAI,YAAY,OAAO;AACrB,UAAI,aAAa,QAAW;AAE1B,eAAO;AAAA,MACT;AAEA,UAAI,OAAO,aAAa,YAAY;AAClC,eAAO,SAAS,OAAO,KAAK,KAAK;AAAA,MACnC;AAEA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AACF","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return <GtmProviderInner config={config}>{children}</GtmProviderInner>;\n};\n\nconst GtmProviderInner = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n client.init();\n return () => {\n client.teardown();\n };\n }, [client, config]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n\n /**\n * Callback executed before GTM initialization.\n * Use this to set consent defaults.\n *\n * @example\n * ```tsx\n * <GtmProvider\n * config={{ containers: 'GTM-XXXXXX' }}\n * onBeforeInit={(client) => {\n * client.setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * }}\n * >\n * ```\n */\n onBeforeInit?: (client: GtmClient) => void;\n\n /**\n * Callback executed after GTM initialization.\n */\n onAfterInit?: (client: GtmClient) => void;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return (\n <GtmProviderInner config={config} onBeforeInit={onBeforeInit} onAfterInit={onAfterInit}>\n {children}\n </GtmProviderInner>\n );\n};\n\nconst GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n // Call onBeforeInit hook for consent defaults\n if (onBeforeInit) {\n onBeforeInit(client);\n }\n\n client.init();\n\n // Call onAfterInit hook\n if (onAfterInit) {\n onAfterInit(client);\n }\n\n return () => {\n client.teardown();\n };\n }, [client, config, onBeforeInit, onAfterInit]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwiedeman/gtm-kit-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "React hooks and provider for GTM Kit - Google Tag Manager integration. Supports React 16.8+.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@jwiedeman/gtm-kit": "^1.2.
|
|
52
|
+
"@jwiedeman/gtm-kit": "^1.2.1"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|