@brandlab-digital/site-next 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics.d.ts +111 -52
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +281 -153
- package/dist/analytics.js.map +1 -1
- package/dist/blog.d.ts +4 -2
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +7 -7
- package/dist/blog.js.map +1 -1
- package/dist/contact/client-entry.d.ts +2 -0
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js +1 -0
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/client.d.ts +12 -22
- package/dist/contact/client.d.ts.map +1 -1
- package/dist/contact/client.js +28 -21
- package/dist/contact/client.js.map +1 -1
- package/dist/contact/form-client.d.ts +148 -0
- package/dist/contact/form-client.d.ts.map +1 -0
- package/dist/contact/form-client.js +234 -0
- package/dist/contact/form-client.js.map +1 -0
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/route-handler.d.ts +32 -0
- package/dist/contact/route-handler.d.ts.map +1 -0
- package/dist/contact/route-handler.js +44 -0
- package/dist/contact/route-handler.js.map +1 -0
- package/dist/contact/server-entry.d.ts +1 -0
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -0
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +5 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +13 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +4 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +29 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +27 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/google-reviews-client.d.ts +12 -1
- package/dist/google-reviews-client.d.ts.map +1 -1
- package/dist/google-reviews-client.js +38 -41
- package/dist/google-reviews-client.js.map +1 -1
- package/dist/google-reviews-ui.d.ts +4 -2
- package/dist/google-reviews-ui.d.ts.map +1 -1
- package/dist/google-reviews-ui.js +10 -9
- package/dist/google-reviews-ui.js.map +1 -1
- package/dist/google-tag-plan.d.ts +1 -4
- package/dist/google-tag-plan.d.ts.map +1 -1
- package/dist/google-tag-plan.js +1 -14
- package/dist/google-tag-plan.js.map +1 -1
- package/dist/google-tag.d.ts +5 -4
- package/dist/google-tag.d.ts.map +1 -1
- package/dist/google-tag.js +5 -4
- package/dist/google-tag.js.map +1 -1
- package/dist/headers.d.ts +21 -28
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +62 -51
- package/dist/headers.js.map +1 -1
- package/dist/instrumentation-client.d.ts +21 -1
- package/dist/instrumentation-client.d.ts.map +1 -1
- package/dist/instrumentation-client.js +62 -7
- package/dist/instrumentation-client.js.map +1 -1
- package/dist/lead-registry.d.ts +34 -0
- package/dist/lead-registry.d.ts.map +1 -0
- package/dist/lead-registry.js +30 -0
- package/dist/lead-registry.js.map +1 -0
- package/dist/lead-reporting.d.ts +37 -0
- package/dist/lead-reporting.d.ts.map +1 -0
- package/dist/lead-reporting.js +74 -0
- package/dist/lead-reporting.js.map +1 -0
- package/dist/metadata.d.ts +10 -11
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +17 -19
- package/dist/metadata.js.map +1 -1
- package/dist/next-config.d.ts +102 -0
- package/dist/next-config.d.ts.map +1 -0
- package/dist/next-config.js +140 -0
- package/dist/next-config.js.map +1 -0
- package/dist/robots.d.ts +14 -6
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +26 -17
- package/dist/robots.js.map +1 -1
- package/dist/sitemaps.d.ts +34 -0
- package/dist/sitemaps.d.ts.map +1 -1
- package/dist/sitemaps.js +66 -1
- package/dist/sitemaps.js.map +1 -1
- package/dist/testing/server-only.d.ts +8 -0
- package/dist/testing/server-only.d.ts.map +1 -0
- package/dist/testing/server-only.js +8 -0
- package/dist/testing/server-only.js.map +1 -0
- package/dist/ui-client.d.ts +1 -12
- package/dist/ui-client.d.ts.map +1 -1
- package/dist/ui-client.js +0 -17
- package/dist/ui-client.js.map +1 -1
- package/dist/ui.d.ts +22 -2
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +67 -6
- package/dist/ui.js.map +1 -1
- package/package.json +16 -3
package/dist/analytics.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type BrandlabAnalyticsContract, type
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
1
|
+
import { type BrandlabAnalyticsContract, type GtagTransport } from '@brandlab-digital/site-core/analytics';
|
|
2
|
+
import type { BrandlabGoogleTagConfig } from './google-tag-plan.js';
|
|
3
|
+
import { getBrowserGtag } from './lead-registry.js';
|
|
4
|
+
export { getBrowserGtag };
|
|
4
5
|
export interface BrandlabBrowserAnalyticsConfig {
|
|
5
6
|
enabled: boolean;
|
|
6
|
-
webVitalsEnabled?: boolean;
|
|
7
7
|
leadDiagnosticsEnabled?: boolean;
|
|
8
8
|
enhancedConversionsEnabled?: boolean;
|
|
9
9
|
measurementId?: string | null;
|
|
@@ -13,64 +13,24 @@ export interface BrandlabBrowserAnalyticsConfig {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Public analytics values that a client bundle must bind through direct,
|
|
16
|
-
* statically
|
|
16
|
+
* statically analysable `process.env.NEXT_PUBLIC_*` property access. Ads keys
|
|
17
|
+
* are bound only when the contract declares Google Ads (and website calls).
|
|
17
18
|
*/
|
|
18
19
|
export interface BrandlabBrowserAnalyticsPublicValues {
|
|
20
|
+
/** `process.env.NEXT_PUBLIC_VERCEL_ENV`, inlined by defineBrandlabNextConfig. */
|
|
21
|
+
deploymentEnvironment: string | null | undefined;
|
|
19
22
|
measurementId: string | null | undefined;
|
|
20
|
-
googleAdsConversionId
|
|
21
|
-
googleAdsConversionLabel
|
|
23
|
+
googleAdsConversionId?: string | null | undefined;
|
|
24
|
+
googleAdsConversionLabel?: string | null | undefined;
|
|
25
|
+
googleAdsWebsiteCallConversionLabel?: string | null | undefined;
|
|
22
26
|
}
|
|
23
27
|
export interface SafeContactAnalyticsParams {
|
|
24
28
|
page_template?: string;
|
|
25
29
|
service_slug?: string;
|
|
26
30
|
location_slug?: string;
|
|
27
31
|
}
|
|
28
|
-
export interface SafeContactAdsParams {
|
|
29
|
-
currency?: string;
|
|
30
|
-
value?: number;
|
|
31
|
-
}
|
|
32
32
|
export declare const PHONE_CALL_PLACEMENTS: readonly ["header", "footer", "hero", "cta", "fab", "contact", "location", "page"];
|
|
33
33
|
export type PhoneCallPlacement = (typeof PHONE_CALL_PLACEMENTS)[number];
|
|
34
|
-
export declare function resolveBrowserAnalyticsConfig(contract: BrandlabAnalyticsContract, publicValues: BrandlabBrowserAnalyticsPublicValues, options: {
|
|
35
|
-
enabled: boolean;
|
|
36
|
-
gtag?: GtagTransport;
|
|
37
|
-
}): BrandlabBrowserAnalyticsConfig;
|
|
38
|
-
export type BrandlabWebVitalName = 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB';
|
|
39
|
-
export type BrandlabWebVitalRating = 'good' | 'needs-improvement' | 'poor';
|
|
40
|
-
export interface BrandlabWebVitalMetric {
|
|
41
|
-
name: BrandlabWebVitalName;
|
|
42
|
-
value: number;
|
|
43
|
-
delta: number;
|
|
44
|
-
rating: BrandlabWebVitalRating;
|
|
45
|
-
navigationType?: string;
|
|
46
|
-
}
|
|
47
|
-
/** Privacy-safe callback for Next's useReportWebVitals hook. */
|
|
48
|
-
export declare function reportBrandlabWebVital(metric: BrandlabWebVitalMetric, config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'webVitalsEnabled' | 'measurementId' | 'gtag'>): boolean;
|
|
49
|
-
export declare function createBrandlabWebVitalsReporter(config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'webVitalsEnabled' | 'measurementId' | 'gtag'>): (metric: BrandlabWebVitalMetric) => boolean;
|
|
50
|
-
export declare function getBrowserGtag(win?: Window): GtagTransport | undefined;
|
|
51
|
-
export declare function isTrackableContactActionResult(result: ContactActionResult): result is Extract<ContactActionResult, {
|
|
52
|
-
ok: true;
|
|
53
|
-
accepted: true;
|
|
54
|
-
}>;
|
|
55
|
-
export declare function trackDeliveredContact(result: ContactActionResult, config: BrandlabBrowserAnalyticsConfig, options: {
|
|
56
|
-
pagePath: string;
|
|
57
|
-
params?: SafeContactAnalyticsParams;
|
|
58
|
-
adsParams?: SafeContactAdsParams;
|
|
59
|
-
}): ContactFormConversionResult;
|
|
60
|
-
/**
|
|
61
|
-
* Sends Enhanced Conversions only after provider acceptance. Email and phone may
|
|
62
|
-
* only be sourced from fields with explicit declarative roles; they never enter
|
|
63
|
-
* the GA4 parameter object.
|
|
64
|
-
*/
|
|
65
|
-
export declare function trackDeliveredContactWithEnhancedConversions(result: ContactActionResult, config: BrandlabBrowserAnalyticsConfig, options: {
|
|
66
|
-
pagePath: string;
|
|
67
|
-
definition: BrandlabContactDefinition;
|
|
68
|
-
formData: FormData;
|
|
69
|
-
/** Live Consent Mode decision for ad_user_data; absence is deny. */
|
|
70
|
-
adUserDataConsent: boolean;
|
|
71
|
-
params?: SafeContactAnalyticsParams;
|
|
72
|
-
adsParams?: SafeContactAdsParams;
|
|
73
|
-
}): ContactFormConversionResult;
|
|
74
34
|
export declare function trackPhoneLinkClick(input: {
|
|
75
35
|
pagePath: string;
|
|
76
36
|
placement: PhoneCallPlacement;
|
|
@@ -99,5 +59,104 @@ export declare function trackLeadCtaClick(input: {
|
|
|
99
59
|
export declare function trackBrandlabBrowserEvent(eventName: string, params: Record<string, unknown>, config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'measurementId' | 'gtag'> & {
|
|
100
60
|
contract: BrandlabAnalyticsContract;
|
|
101
61
|
}): boolean;
|
|
102
|
-
|
|
62
|
+
type PageDimension = 'service_slug' | 'location_slug';
|
|
63
|
+
/**
|
|
64
|
+
* One row of the page-template table. Rows are tried in order against the
|
|
65
|
+
* page path's segments (query, hash, empty and trailing segments ignored) and
|
|
66
|
+
* the first match wins; no match reports no page dimensions.
|
|
67
|
+
*
|
|
68
|
+
* `path` segments: a literal matches itself; `:name` matches any one segment,
|
|
69
|
+
* and `:service_slug` / `:location_slug` also report it (lowercased, or
|
|
70
|
+
* omitted when it is not a safe slug, without failing the row); `**` matches
|
|
71
|
+
* zero or more segments and may appear once, anywhere.
|
|
72
|
+
*/
|
|
73
|
+
export interface BrandlabAnalyticsPageTemplate {
|
|
74
|
+
path: string;
|
|
75
|
+
template: string;
|
|
76
|
+
/** Constant dimensions for every page the row matches. */
|
|
77
|
+
params?: Readonly<Partial<Record<PageDimension, string>>>;
|
|
78
|
+
/**
|
|
79
|
+
* Constraints on `:name` segments, tested against the segment exactly as it
|
|
80
|
+
* appears in the path: a list of allowed values or a regular expression. A
|
|
81
|
+
* segment that fails its constraint skips the row.
|
|
82
|
+
*/
|
|
83
|
+
values?: Readonly<Record<string, readonly string[] | RegExp>>;
|
|
84
|
+
}
|
|
85
|
+
/** What a declared lead value sees for one accepted enquiry. */
|
|
86
|
+
export interface BrandlabLeadContext {
|
|
87
|
+
/** The form's page path, as useBrandlabContactForm received it. */
|
|
88
|
+
readonly pagePath: string;
|
|
89
|
+
/** The page's contact_form dimensions from the page-template table. */
|
|
90
|
+
readonly pageParams: SafeContactAnalyticsParams;
|
|
91
|
+
/** The accepted enquiry's fields; they are never sent to analytics. */
|
|
92
|
+
readonly formData: FormData;
|
|
93
|
+
}
|
|
94
|
+
export interface BrandlabGoogleAdsLeadConversion {
|
|
95
|
+
/**
|
|
96
|
+
* Conversion value: a number, or a function evaluated for each accepted
|
|
97
|
+
* enquiry. A function that throws or returns anything but a finite,
|
|
98
|
+
* non-negative number sends the conversion without a value.
|
|
99
|
+
*/
|
|
100
|
+
value?: number | ((lead: BrandlabLeadContext) => number | undefined);
|
|
101
|
+
/** ISO 4217 currency code, for example `AUD`. */
|
|
102
|
+
currency?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* How the site reports an accepted enquiry, declared once. Every accepted
|
|
106
|
+
* enquiry sends one GA4 `contact_form` (page dimensions from the page-template
|
|
107
|
+
* table) and, when the contract declares Google Ads, one form conversion with
|
|
108
|
+
* `send_to` and `transaction_id` plus the value and currency declared here.
|
|
109
|
+
*/
|
|
110
|
+
export interface BrandlabLeadReporting {
|
|
111
|
+
googleAdsConversion?: BrandlabGoogleAdsLeadConversion;
|
|
112
|
+
}
|
|
113
|
+
export interface DefineBrandlabAnalyticsOptions {
|
|
114
|
+
contract: BrandlabAnalyticsContract;
|
|
115
|
+
publicValues: BrandlabBrowserAnalyticsPublicValues;
|
|
116
|
+
/** Classifies page paths for contact_form and lead_form_start dimensions. */
|
|
117
|
+
pageTemplates: readonly BrandlabAnalyticsPageTemplate[];
|
|
118
|
+
/** The Google Ads form-conversion value and currency, if the site sends them. */
|
|
119
|
+
leads?: BrandlabLeadReporting;
|
|
120
|
+
}
|
|
121
|
+
export interface BrandlabAnalytics {
|
|
122
|
+
readonly contract: BrandlabAnalyticsContract;
|
|
123
|
+
/** True only on the Vercel production deployment. */
|
|
124
|
+
readonly enabled: boolean;
|
|
125
|
+
/** Props for the single `<BrandlabGoogleTag {...analytics.googleTag} />` in the root layout. */
|
|
126
|
+
readonly googleTag: BrandlabGoogleTagConfig;
|
|
127
|
+
/** Tracking configuration; the page's gtag function is resolved when an event is sent. */
|
|
128
|
+
readonly config: BrandlabBrowserAnalyticsConfig;
|
|
129
|
+
/** Privacy-safe page_template, service_slug and location_slug for a path. */
|
|
130
|
+
pageParams(pagePath: string): SafeContactAnalyticsParams;
|
|
131
|
+
trackPhoneLinkClick(input: {
|
|
132
|
+
pagePath: string;
|
|
133
|
+
placement: PhoneCallPlacement;
|
|
134
|
+
}): boolean;
|
|
135
|
+
trackLeadFormStart(input: {
|
|
136
|
+
pagePath: string;
|
|
137
|
+
formId: string;
|
|
138
|
+
}): boolean;
|
|
139
|
+
trackLeadFormError(input: {
|
|
140
|
+
pagePath: string;
|
|
141
|
+
formId: string;
|
|
142
|
+
stage: LeadFormErrorStage;
|
|
143
|
+
errorCount: number;
|
|
144
|
+
}): boolean;
|
|
145
|
+
trackLeadCtaClick(input: {
|
|
146
|
+
pagePath: string;
|
|
147
|
+
ctaId: string;
|
|
148
|
+
placement: LeadCtaPlacement;
|
|
149
|
+
}): boolean;
|
|
150
|
+
/** Sends an event declared in the contract's additionalEvents, filtered by its parameter policy. */
|
|
151
|
+
trackEvent(eventName: string, params: Record<string, unknown>): boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The project's single analytics binding (`src/config/analytics.ts`). It is
|
|
155
|
+
* safe in server and browser code: the root layout renders
|
|
156
|
+
* `analytics.googleTag`, `src/instrumentation-client.ts` installs the phone
|
|
157
|
+
* listener, and `useBrandlabContactForm` reports each accepted enquiry exactly
|
|
158
|
+
* as `pageTemplates` and `leads` declare, plus the lead diagnostics. Analytics
|
|
159
|
+
* runs only on the Vercel production deployment.
|
|
160
|
+
*/
|
|
161
|
+
export declare function defineBrandlabAnalytics(options: DefineBrandlabAnalyticsOptions): BrandlabAnalytics;
|
|
103
162
|
//# sourceMappingURL=analytics.d.ts.map
|
package/dist/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/analytics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { defineAnalyticsContract, sendBrandlabAnalyticsEvent, } from '@brandlab-digital/site-core/analytics';
|
|
2
|
+
import { resolveBrandlabDeployment } from '@brandlab-digital/site-core/site';
|
|
3
|
+
import { getBrowserGtag, registerLeadBinding, safePagePath, safeSlug, } from './lead-registry.js';
|
|
4
|
+
export { getBrowserGtag };
|
|
4
5
|
export const PHONE_CALL_PLACEMENTS = [
|
|
5
6
|
'header',
|
|
6
7
|
'footer',
|
|
@@ -11,156 +12,6 @@ export const PHONE_CALL_PLACEMENTS = [
|
|
|
11
12
|
'location',
|
|
12
13
|
'page',
|
|
13
14
|
];
|
|
14
|
-
export function resolveBrowserAnalyticsConfig(contract, publicValues, options) {
|
|
15
|
-
defineAnalyticsContract(contract);
|
|
16
|
-
return {
|
|
17
|
-
enabled: options.enabled,
|
|
18
|
-
webVitalsEnabled: contract.webVitals,
|
|
19
|
-
leadDiagnosticsEnabled: contract.leadDiagnostics?.enabled === true,
|
|
20
|
-
enhancedConversionsEnabled: contract.googleAds?.enhancedConversions?.mode === 'consent-required',
|
|
21
|
-
measurementId: publicValues.measurementId,
|
|
22
|
-
googleAdsConversionId: contract.googleAds
|
|
23
|
-
? publicValues.googleAdsConversionId
|
|
24
|
-
: undefined,
|
|
25
|
-
googleAdsConversionLabel: contract.googleAds
|
|
26
|
-
? publicValues.googleAdsConversionLabel
|
|
27
|
-
: undefined,
|
|
28
|
-
gtag: options.gtag,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const WEB_VITAL_NAMES = new Set(['CLS', 'FCP', 'INP', 'LCP', 'TTFB']);
|
|
32
|
-
const WEB_VITAL_RATINGS = new Set([
|
|
33
|
-
'good',
|
|
34
|
-
'needs-improvement',
|
|
35
|
-
'poor',
|
|
36
|
-
]);
|
|
37
|
-
const WEB_VITAL_NAVIGATION_TYPES = new Set([
|
|
38
|
-
'navigate',
|
|
39
|
-
'reload',
|
|
40
|
-
'back-forward',
|
|
41
|
-
'back-forward-cache',
|
|
42
|
-
'prerender',
|
|
43
|
-
'restore',
|
|
44
|
-
]);
|
|
45
|
-
/** Privacy-safe callback for Next's useReportWebVitals hook. */
|
|
46
|
-
export function reportBrandlabWebVital(metric, config) {
|
|
47
|
-
if (!WEB_VITAL_NAMES.has(metric.name) ||
|
|
48
|
-
!WEB_VITAL_RATINGS.has(metric.rating) ||
|
|
49
|
-
!Number.isFinite(metric.value) ||
|
|
50
|
-
metric.value < 0 ||
|
|
51
|
-
!Number.isFinite(metric.delta)) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
const navigationType = WEB_VITAL_NAVIGATION_TYPES.has(metric.navigationType ?? '')
|
|
55
|
-
? metric.navigationType
|
|
56
|
-
: undefined;
|
|
57
|
-
return sendBrandlabAnalyticsEvent({
|
|
58
|
-
enabled: config.enabled && config.webVitalsEnabled !== false,
|
|
59
|
-
measurementId: config.measurementId,
|
|
60
|
-
gtag: config.gtag ?? getBrowserGtag(),
|
|
61
|
-
eventName: 'web_vital',
|
|
62
|
-
params: {
|
|
63
|
-
metric_name: metric.name,
|
|
64
|
-
metric_value: metric.value,
|
|
65
|
-
metric_delta: metric.delta,
|
|
66
|
-
metric_rating: metric.rating,
|
|
67
|
-
...(navigationType ? { navigation_type: navigationType } : {}),
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
export function createBrandlabWebVitalsReporter(config) {
|
|
72
|
-
return (metric) => reportBrandlabWebVital(metric, config);
|
|
73
|
-
}
|
|
74
|
-
export function getBrowserGtag(win = window) {
|
|
75
|
-
const candidate = win.gtag;
|
|
76
|
-
return typeof candidate === 'function' ? candidate : undefined;
|
|
77
|
-
}
|
|
78
|
-
function safePagePath(value) {
|
|
79
|
-
const path = value.trim().split(/[?#]/, 1)[0];
|
|
80
|
-
if (!path || path.length > 500)
|
|
81
|
-
return '/';
|
|
82
|
-
try {
|
|
83
|
-
return normalizeBrandlabRoutePath(path);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return '/';
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function safeSlug(value) {
|
|
90
|
-
const normalized = value?.trim().toLowerCase();
|
|
91
|
-
return normalized && /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(normalized) && normalized.length <= 80
|
|
92
|
-
? normalized
|
|
93
|
-
: undefined;
|
|
94
|
-
}
|
|
95
|
-
function sanitizeContactParams(params) {
|
|
96
|
-
return {
|
|
97
|
-
...(safeSlug(params?.page_template)
|
|
98
|
-
? { page_template: safeSlug(params?.page_template) }
|
|
99
|
-
: {}),
|
|
100
|
-
...(safeSlug(params?.service_slug)
|
|
101
|
-
? { service_slug: safeSlug(params?.service_slug) }
|
|
102
|
-
: {}),
|
|
103
|
-
...(safeSlug(params?.location_slug)
|
|
104
|
-
? { location_slug: safeSlug(params?.location_slug) }
|
|
105
|
-
: {}),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function sanitizeAdsParams(params) {
|
|
109
|
-
return {
|
|
110
|
-
...(params?.currency && /^[A-Z]{3}$/.test(params.currency)
|
|
111
|
-
? { currency: params.currency }
|
|
112
|
-
: {}),
|
|
113
|
-
...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0
|
|
114
|
-
? { value: params.value }
|
|
115
|
-
: {}),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
export function isTrackableContactActionResult(result) {
|
|
119
|
-
return result.ok && result.accepted && result.trackConversion;
|
|
120
|
-
}
|
|
121
|
-
export function trackDeliveredContact(result, config, options) {
|
|
122
|
-
if (!isTrackableContactActionResult(result)) {
|
|
123
|
-
return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false };
|
|
124
|
-
}
|
|
125
|
-
return sendContactFormConversion({
|
|
126
|
-
...config,
|
|
127
|
-
gtag: config.gtag ?? getBrowserGtag(),
|
|
128
|
-
pagePath: safePagePath(options.pagePath),
|
|
129
|
-
submissionId: result.confirmationId,
|
|
130
|
-
params: sanitizeContactParams(options.params),
|
|
131
|
-
adsParams: sanitizeAdsParams(options.adsParams),
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
function explicitRoleValue(definition, formData, role) {
|
|
135
|
-
const field = definition.fields.find((candidate) => candidate.role === role);
|
|
136
|
-
if (!field)
|
|
137
|
-
return undefined;
|
|
138
|
-
const value = formData.get(field.name);
|
|
139
|
-
return typeof value === 'string' ? value : undefined;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Sends Enhanced Conversions only after provider acceptance. Email and phone may
|
|
143
|
-
* only be sourced from fields with explicit declarative roles; they never enter
|
|
144
|
-
* the GA4 parameter object.
|
|
145
|
-
*/
|
|
146
|
-
export function trackDeliveredContactWithEnhancedConversions(result, config, options) {
|
|
147
|
-
if (!isTrackableContactActionResult(result)) {
|
|
148
|
-
return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false };
|
|
149
|
-
}
|
|
150
|
-
const email = normalizeEnhancedConversionEmail(explicitRoleValue(options.definition, options.formData, 'email'));
|
|
151
|
-
const phone = normalizeEnhancedConversionPhone(explicitRoleValue(options.definition, options.formData, 'phone'));
|
|
152
|
-
return sendContactFormConversion({
|
|
153
|
-
...config,
|
|
154
|
-
gtag: config.gtag ?? getBrowserGtag(),
|
|
155
|
-
pagePath: safePagePath(options.pagePath),
|
|
156
|
-
submissionId: result.confirmationId,
|
|
157
|
-
params: sanitizeContactParams(options.params),
|
|
158
|
-
adsParams: sanitizeAdsParams(options.adsParams),
|
|
159
|
-
enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,
|
|
160
|
-
adUserDataConsent: options.adUserDataConsent === true,
|
|
161
|
-
...((email || phone) ? { enhancedConversion: { email, phone } } : {}),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
15
|
export function trackPhoneLinkClick(input, config) {
|
|
165
16
|
if (!PHONE_CALL_PLACEMENTS.includes(input.placement))
|
|
166
17
|
return false;
|
|
@@ -277,4 +128,281 @@ export function trackBrandlabBrowserEvent(eventName, params, config) {
|
|
|
277
128
|
params: safeParams,
|
|
278
129
|
});
|
|
279
130
|
}
|
|
131
|
+
const PAGE_DIMENSIONS = ['service_slug', 'location_slug'];
|
|
132
|
+
const MAX_PAGE_TEMPLATE_ROWS = 5_000;
|
|
133
|
+
function compilePageTemplate(row, index) {
|
|
134
|
+
const field = `pageTemplates[${index}]`;
|
|
135
|
+
if (!row || typeof row !== 'object')
|
|
136
|
+
throw new Error(`${field} must be a page-template row.`);
|
|
137
|
+
const unsupported = Object.keys(row).filter((key) => !['path', 'template', 'params', 'values'].includes(key));
|
|
138
|
+
if (unsupported.length > 0) {
|
|
139
|
+
throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.`);
|
|
140
|
+
}
|
|
141
|
+
if (typeof row.path !== 'string' || !row.path.startsWith('/') || row.path.includes('//')) {
|
|
142
|
+
throw new Error(`${field}.path must be a root-relative pattern.`);
|
|
143
|
+
}
|
|
144
|
+
const template = safeSlug(row.template);
|
|
145
|
+
if (!template || template !== row.template) {
|
|
146
|
+
throw new Error(`${field}.template must be a lowercase slug.`);
|
|
147
|
+
}
|
|
148
|
+
const parts = row.path === '/' ? [] : row.path.replace(/\/$/, '').slice(1).split('/');
|
|
149
|
+
if (parts.filter((part) => part === '**').length > 1) {
|
|
150
|
+
throw new Error(`${field}.path may use ** only once.`);
|
|
151
|
+
}
|
|
152
|
+
const names = new Set();
|
|
153
|
+
const segments = parts.map((part) => {
|
|
154
|
+
if (part === '**')
|
|
155
|
+
return '**';
|
|
156
|
+
if (part.startsWith(':')) {
|
|
157
|
+
const name = part.slice(1);
|
|
158
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(name))
|
|
159
|
+
throw new Error(`${field}.path has an invalid parameter "${part}".`);
|
|
160
|
+
if (names.has(name))
|
|
161
|
+
throw new Error(`${field}.path repeats ${part}.`);
|
|
162
|
+
names.add(name);
|
|
163
|
+
return PAGE_DIMENSIONS.includes(name)
|
|
164
|
+
? { kind: 'parameter', name, dimension: name }
|
|
165
|
+
: { kind: 'parameter', name };
|
|
166
|
+
}
|
|
167
|
+
if (!/^[A-Za-z0-9._~-]+$/.test(part))
|
|
168
|
+
throw new Error(`${field}.path has an invalid segment "${part}".`);
|
|
169
|
+
return { kind: 'literal', value: part };
|
|
170
|
+
});
|
|
171
|
+
const params = {};
|
|
172
|
+
for (const [key, value] of Object.entries(row.params ?? {})) {
|
|
173
|
+
const dimension = key;
|
|
174
|
+
if (!PAGE_DIMENSIONS.includes(dimension) || names.has(dimension)) {
|
|
175
|
+
throw new Error(`${field}.params.${key} is not an uncaptured page dimension.`);
|
|
176
|
+
}
|
|
177
|
+
if (typeof value !== 'string' || safeSlug(value) !== value) {
|
|
178
|
+
throw new Error(`${field}.params.${key} must be a lowercase slug.`);
|
|
179
|
+
}
|
|
180
|
+
params[dimension] = value;
|
|
181
|
+
}
|
|
182
|
+
for (const [name, allowed] of Object.entries(row.values ?? {})) {
|
|
183
|
+
if (!names.has(name))
|
|
184
|
+
throw new Error(`${field}.values.${name} needs :${name} in the path.`);
|
|
185
|
+
let constraint;
|
|
186
|
+
if (allowed instanceof RegExp) {
|
|
187
|
+
if (allowed.global || allowed.sticky) {
|
|
188
|
+
throw new Error(`${field}.values.${name} must be a regular expression without the g or y flag.`);
|
|
189
|
+
}
|
|
190
|
+
constraint = allowed;
|
|
191
|
+
}
|
|
192
|
+
else if (Array.isArray(allowed) && allowed.every((value) => typeof value === 'string' && value.length > 0)) {
|
|
193
|
+
constraint = new Set(allowed);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
throw new Error(`${field}.values.${name} must list segment values or be a regular expression.`);
|
|
197
|
+
}
|
|
198
|
+
for (const segment of segments) {
|
|
199
|
+
if (segment !== '**' && segment.kind === 'parameter' && segment.name === name)
|
|
200
|
+
segment.allowed = constraint;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const rest = segments.indexOf('**');
|
|
204
|
+
const compiled = segments.filter((segment) => segment !== '**');
|
|
205
|
+
return {
|
|
206
|
+
head: rest === -1 ? compiled : compiled.slice(0, rest),
|
|
207
|
+
...(rest === -1 ? {} : { tail: compiled.slice(rest) }),
|
|
208
|
+
template,
|
|
209
|
+
params,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function pathSegments(pagePath) {
|
|
213
|
+
return String(pagePath).split(/[?#]/, 1)[0].split('/').filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
function matchSegment(segment, part, params) {
|
|
216
|
+
if (segment.kind === 'literal')
|
|
217
|
+
return segment.value === part;
|
|
218
|
+
const { allowed } = segment;
|
|
219
|
+
if (allowed && !(allowed instanceof RegExp ? allowed.test(part) : allowed.has(part)))
|
|
220
|
+
return false;
|
|
221
|
+
if (segment.dimension) {
|
|
222
|
+
const value = safeSlug(part);
|
|
223
|
+
if (value)
|
|
224
|
+
params[segment.dimension] = value;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
function matchPageTemplate(row, parts) {
|
|
229
|
+
const fixed = row.head.length + (row.tail?.length ?? 0);
|
|
230
|
+
if (row.tail ? parts.length < fixed : parts.length !== fixed)
|
|
231
|
+
return undefined;
|
|
232
|
+
const params = { page_template: row.template, ...row.params };
|
|
233
|
+
for (let index = 0; index < row.head.length; index += 1) {
|
|
234
|
+
if (!matchSegment(row.head[index], parts[index], params))
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const offset = parts.length - (row.tail?.length ?? 0);
|
|
238
|
+
for (let index = 0; index < (row.tail?.length ?? 0); index += 1) {
|
|
239
|
+
if (!matchSegment(row.tail[index], parts[offset + index], params))
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return params;
|
|
243
|
+
}
|
|
244
|
+
function compileLeadReporting(contract, leads) {
|
|
245
|
+
if (leads === undefined)
|
|
246
|
+
return () => ({});
|
|
247
|
+
if (!leads || typeof leads !== 'object' || Array.isArray(leads)) {
|
|
248
|
+
throw new Error('leads must be a lead-reporting declaration.');
|
|
249
|
+
}
|
|
250
|
+
const unsupported = Object.keys(leads).filter((key) => key !== 'googleAdsConversion');
|
|
251
|
+
if (unsupported.length > 0)
|
|
252
|
+
throw new Error(`leads contains unsupported keys: ${unsupported.join(', ')}.`);
|
|
253
|
+
const conversion = leads.googleAdsConversion;
|
|
254
|
+
if (conversion === undefined)
|
|
255
|
+
return () => ({});
|
|
256
|
+
if (!conversion || typeof conversion !== 'object' || Array.isArray(conversion)) {
|
|
257
|
+
throw new Error('leads.googleAdsConversion must declare value and/or currency.');
|
|
258
|
+
}
|
|
259
|
+
if (!contract.googleAds) {
|
|
260
|
+
throw new Error('leads.googleAdsConversion needs googleAds in the analytics contract.');
|
|
261
|
+
}
|
|
262
|
+
const unsupportedConversion = Object.keys(conversion).filter((key) => key !== 'value' && key !== 'currency');
|
|
263
|
+
if (unsupportedConversion.length > 0) {
|
|
264
|
+
throw new Error(`leads.googleAdsConversion contains unsupported keys: ${unsupportedConversion.join(', ')}.`);
|
|
265
|
+
}
|
|
266
|
+
const { value, currency } = conversion;
|
|
267
|
+
if (currency !== undefined && (typeof currency !== 'string' || !/^[A-Z]{3}$/.test(currency))) {
|
|
268
|
+
throw new Error('leads.googleAdsConversion.currency must be an ISO 4217 code such as AUD.');
|
|
269
|
+
}
|
|
270
|
+
if (value !== undefined &&
|
|
271
|
+
typeof value !== 'function' &&
|
|
272
|
+
(typeof value !== 'number' || !Number.isFinite(value) || value < 0)) {
|
|
273
|
+
throw new Error('leads.googleAdsConversion.value must be a non-negative number or a function.');
|
|
274
|
+
}
|
|
275
|
+
return (lead) => {
|
|
276
|
+
let resolved = value;
|
|
277
|
+
if (typeof value === 'function') {
|
|
278
|
+
try {
|
|
279
|
+
resolved = value(lead);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
resolved = undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...(currency ? { currency } : {}),
|
|
287
|
+
...(typeof resolved === 'number' ? { value: resolved } : {}),
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const PUBLIC_VALUE_KEYS = [
|
|
292
|
+
'deploymentEnvironment',
|
|
293
|
+
'measurementId',
|
|
294
|
+
'googleAdsConversionId',
|
|
295
|
+
'googleAdsConversionLabel',
|
|
296
|
+
'googleAdsWebsiteCallConversionLabel',
|
|
297
|
+
];
|
|
298
|
+
function assertPublicValues(contract, publicValues) {
|
|
299
|
+
if (!publicValues || typeof publicValues !== 'object' || Array.isArray(publicValues)) {
|
|
300
|
+
throw new Error('publicValues must bind the NEXT_PUBLIC_* analytics values.');
|
|
301
|
+
}
|
|
302
|
+
const required = new Set(['deploymentEnvironment', 'measurementId']);
|
|
303
|
+
if (contract.googleAds) {
|
|
304
|
+
required.add('googleAdsConversionId');
|
|
305
|
+
required.add('googleAdsConversionLabel');
|
|
306
|
+
if (contract.googleAds.websiteCalls)
|
|
307
|
+
required.add('googleAdsWebsiteCallConversionLabel');
|
|
308
|
+
}
|
|
309
|
+
for (const key of Object.keys(publicValues)) {
|
|
310
|
+
if (!PUBLIC_VALUE_KEYS.includes(key)) {
|
|
311
|
+
throw new Error(`publicValues contains unsupported key "${key}".`);
|
|
312
|
+
}
|
|
313
|
+
if (!required.has(key)) {
|
|
314
|
+
throw new Error(`publicValues.${key} is bound but the analytics contract does not declare it.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const key of required) {
|
|
318
|
+
if (!Object.hasOwn(publicValues, key)) {
|
|
319
|
+
throw new Error(`publicValues.${key} must be bound with literal process.env property access.`);
|
|
320
|
+
}
|
|
321
|
+
const value = publicValues[key];
|
|
322
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
323
|
+
throw new Error(`publicValues.${key} must be a string or undefined.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const browserEnvKeys = [
|
|
327
|
+
contract.measurementIdEnv,
|
|
328
|
+
contract.googleAds?.conversionIdEnv,
|
|
329
|
+
contract.googleAds?.contactConversionLabelEnv,
|
|
330
|
+
contract.googleAds?.websiteCalls?.conversionLabelEnv,
|
|
331
|
+
].filter((key) => Boolean(key));
|
|
332
|
+
const privateKey = browserEnvKeys.find((key) => !key.startsWith('NEXT_PUBLIC_'));
|
|
333
|
+
if (privateKey) {
|
|
334
|
+
throw new Error(`${privateKey} is read in the browser and must be a NEXT_PUBLIC_ variable.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function publicValue(value) {
|
|
338
|
+
const trimmed = value?.trim();
|
|
339
|
+
return trimmed || undefined;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The project's single analytics binding (`src/config/analytics.ts`). It is
|
|
343
|
+
* safe in server and browser code: the root layout renders
|
|
344
|
+
* `analytics.googleTag`, `src/instrumentation-client.ts` installs the phone
|
|
345
|
+
* listener, and `useBrandlabContactForm` reports each accepted enquiry exactly
|
|
346
|
+
* as `pageTemplates` and `leads` declare, plus the lead diagnostics. Analytics
|
|
347
|
+
* runs only on the Vercel production deployment.
|
|
348
|
+
*/
|
|
349
|
+
export function defineBrandlabAnalytics(options) {
|
|
350
|
+
if (!options || typeof options !== 'object') {
|
|
351
|
+
throw new TypeError('defineBrandlabAnalytics requires contract, publicValues and pageTemplates.');
|
|
352
|
+
}
|
|
353
|
+
const contract = defineAnalyticsContract(options.contract);
|
|
354
|
+
assertPublicValues(contract, options.publicValues);
|
|
355
|
+
if (!Array.isArray(options.pageTemplates) || options.pageTemplates.length > MAX_PAGE_TEMPLATE_ROWS) {
|
|
356
|
+
throw new Error(`pageTemplates must list at most ${MAX_PAGE_TEMPLATE_ROWS} rows.`);
|
|
357
|
+
}
|
|
358
|
+
const pageTemplates = options.pageTemplates.map(compilePageTemplate);
|
|
359
|
+
const leadAdsParams = compileLeadReporting(contract, options.leads);
|
|
360
|
+
const values = options.publicValues;
|
|
361
|
+
const enabled = resolveBrandlabDeployment({ NEXT_PUBLIC_VERCEL_ENV: publicValue(values.deploymentEnvironment) })
|
|
362
|
+
.environment === 'production';
|
|
363
|
+
const measurementId = publicValue(values.measurementId);
|
|
364
|
+
const googleAdsConversionId = contract.googleAds ? publicValue(values.googleAdsConversionId) : undefined;
|
|
365
|
+
const config = Object.freeze({
|
|
366
|
+
enabled,
|
|
367
|
+
// Privacy-safe lead_form_start / lead_form_error diagnostics are on for every site.
|
|
368
|
+
leadDiagnosticsEnabled: true,
|
|
369
|
+
enhancedConversionsEnabled: contract.googleAds?.enhancedConversions?.mode === 'consent-required',
|
|
370
|
+
measurementId,
|
|
371
|
+
googleAdsConversionId,
|
|
372
|
+
googleAdsConversionLabel: contract.googleAds
|
|
373
|
+
? publicValue(values.googleAdsConversionLabel)
|
|
374
|
+
: undefined,
|
|
375
|
+
});
|
|
376
|
+
const googleTag = Object.freeze({
|
|
377
|
+
enabled,
|
|
378
|
+
measurementId,
|
|
379
|
+
googleAdsConversionId,
|
|
380
|
+
googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls
|
|
381
|
+
? publicValue(values.googleAdsWebsiteCallConversionLabel)
|
|
382
|
+
: undefined,
|
|
383
|
+
});
|
|
384
|
+
const pageParams = (pagePath) => {
|
|
385
|
+
const parts = pathSegments(pagePath);
|
|
386
|
+
for (const row of pageTemplates) {
|
|
387
|
+
const params = matchPageTemplate(row, parts);
|
|
388
|
+
if (params)
|
|
389
|
+
return params;
|
|
390
|
+
}
|
|
391
|
+
return {};
|
|
392
|
+
};
|
|
393
|
+
const binding = Object.freeze({
|
|
394
|
+
contract,
|
|
395
|
+
enabled,
|
|
396
|
+
googleTag,
|
|
397
|
+
config,
|
|
398
|
+
pageParams,
|
|
399
|
+
trackPhoneLinkClick: (input) => trackPhoneLinkClick(input, config),
|
|
400
|
+
trackLeadFormStart: (input) => trackLeadFormStart({ ...input, pageTemplate: pageParams(input.pagePath).page_template }, config),
|
|
401
|
+
trackLeadFormError: (input) => trackLeadFormError(input, config),
|
|
402
|
+
trackLeadCtaClick: (input) => trackLeadCtaClick(input, config),
|
|
403
|
+
trackEvent: (eventName, params) => trackBrandlabBrowserEvent(eventName, params, { ...config, contract }),
|
|
404
|
+
});
|
|
405
|
+
registerLeadBinding(binding, { config, pageParams, adsParams: leadAdsParams });
|
|
406
|
+
return binding;
|
|
407
|
+
}
|
|
280
408
|
//# sourceMappingURL=analytics.js.map
|