@brandlab-digital/site-next 2.1.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.
Files changed (96) hide show
  1. package/dist/analytics.d.ts +111 -52
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +281 -153
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/blog.d.ts +4 -2
  6. package/dist/blog.d.ts.map +1 -1
  7. package/dist/blog.js +7 -7
  8. package/dist/blog.js.map +1 -1
  9. package/dist/contact/client-entry.d.ts +2 -0
  10. package/dist/contact/client-entry.d.ts.map +1 -1
  11. package/dist/contact/client-entry.js +1 -0
  12. package/dist/contact/client-entry.js.map +1 -1
  13. package/dist/contact/client.d.ts +12 -22
  14. package/dist/contact/client.d.ts.map +1 -1
  15. package/dist/contact/client.js +28 -21
  16. package/dist/contact/client.js.map +1 -1
  17. package/dist/contact/form-client.d.ts +148 -0
  18. package/dist/contact/form-client.d.ts.map +1 -0
  19. package/dist/contact/form-client.js +234 -0
  20. package/dist/contact/form-client.js.map +1 -0
  21. package/dist/contact/index.d.ts +1 -1
  22. package/dist/contact/index.d.ts.map +1 -1
  23. package/dist/contact/index.js +1 -1
  24. package/dist/contact/index.js.map +1 -1
  25. package/dist/contact/route-handler.d.ts +32 -0
  26. package/dist/contact/route-handler.d.ts.map +1 -0
  27. package/dist/contact/route-handler.js +44 -0
  28. package/dist/contact/route-handler.js.map +1 -0
  29. package/dist/contact/server-entry.d.ts +1 -0
  30. package/dist/contact/server-entry.d.ts.map +1 -1
  31. package/dist/contact/server-entry.js +1 -0
  32. package/dist/contact/server-entry.js.map +1 -1
  33. package/dist/contact/server.d.ts +5 -0
  34. package/dist/contact/server.d.ts.map +1 -1
  35. package/dist/contact/server.js +13 -7
  36. package/dist/contact/server.js.map +1 -1
  37. package/dist/contact/shared.d.ts +4 -0
  38. package/dist/contact/shared.d.ts.map +1 -1
  39. package/dist/contact/shared.js.map +1 -1
  40. package/dist/contact/testing.d.ts +29 -0
  41. package/dist/contact/testing.d.ts.map +1 -1
  42. package/dist/contact/testing.js +27 -0
  43. package/dist/contact/testing.js.map +1 -1
  44. package/dist/google-tag-plan.d.ts +1 -4
  45. package/dist/google-tag-plan.d.ts.map +1 -1
  46. package/dist/google-tag-plan.js +1 -14
  47. package/dist/google-tag-plan.js.map +1 -1
  48. package/dist/google-tag.d.ts +5 -4
  49. package/dist/google-tag.d.ts.map +1 -1
  50. package/dist/google-tag.js +5 -4
  51. package/dist/google-tag.js.map +1 -1
  52. package/dist/headers.d.ts +21 -28
  53. package/dist/headers.d.ts.map +1 -1
  54. package/dist/headers.js +62 -51
  55. package/dist/headers.js.map +1 -1
  56. package/dist/instrumentation-client.d.ts +21 -1
  57. package/dist/instrumentation-client.d.ts.map +1 -1
  58. package/dist/instrumentation-client.js +62 -7
  59. package/dist/instrumentation-client.js.map +1 -1
  60. package/dist/lead-registry.d.ts +34 -0
  61. package/dist/lead-registry.d.ts.map +1 -0
  62. package/dist/lead-registry.js +30 -0
  63. package/dist/lead-registry.js.map +1 -0
  64. package/dist/lead-reporting.d.ts +37 -0
  65. package/dist/lead-reporting.d.ts.map +1 -0
  66. package/dist/lead-reporting.js +74 -0
  67. package/dist/lead-reporting.js.map +1 -0
  68. package/dist/metadata.d.ts +10 -11
  69. package/dist/metadata.d.ts.map +1 -1
  70. package/dist/metadata.js +17 -19
  71. package/dist/metadata.js.map +1 -1
  72. package/dist/next-config.d.ts +102 -0
  73. package/dist/next-config.d.ts.map +1 -0
  74. package/dist/next-config.js +140 -0
  75. package/dist/next-config.js.map +1 -0
  76. package/dist/robots.d.ts +14 -6
  77. package/dist/robots.d.ts.map +1 -1
  78. package/dist/robots.js +26 -17
  79. package/dist/robots.js.map +1 -1
  80. package/dist/sitemaps.d.ts +34 -0
  81. package/dist/sitemaps.d.ts.map +1 -1
  82. package/dist/sitemaps.js +66 -1
  83. package/dist/sitemaps.js.map +1 -1
  84. package/dist/testing/server-only.d.ts +8 -0
  85. package/dist/testing/server-only.d.ts.map +1 -0
  86. package/dist/testing/server-only.js +8 -0
  87. package/dist/testing/server-only.js.map +1 -0
  88. package/dist/ui-client.d.ts +1 -12
  89. package/dist/ui-client.d.ts.map +1 -1
  90. package/dist/ui-client.js +0 -17
  91. package/dist/ui-client.js.map +1 -1
  92. package/dist/ui.d.ts +22 -2
  93. package/dist/ui.d.ts.map +1 -1
  94. package/dist/ui.js +67 -6
  95. package/dist/ui.js.map +1 -1
  96. package/package.json +16 -3
@@ -1,9 +1,9 @@
1
- import { type BrandlabAnalyticsContract, type ContactFormConversionResult, type GtagTransport } from '@brandlab-digital/site-core/analytics';
2
- import type { ContactActionResult } from '@brandlab-digital/site-core/contact';
3
- import type { BrandlabContactDefinition } from './contact/shared.js';
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 analyzable `process.env.NEXT_PUBLIC_*` property access.
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: string | null | undefined;
21
- googleAdsConversionLabel: string | null | undefined;
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
- export {};
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
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EACnB,MAAM,uCAAuC,CAAA;AAE9C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AACnE,OAAO,EACL,cAAc,EAKf,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,oCAAoC;IACnD,iFAAiF;IACjF,qBAAqB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACxC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACjD,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACpD,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAChE;AAED,MAAM,WAAW,0BAA0B;IACzC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,qBAAqB,oFASxB,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,kBAAkB,CAAA;CAAE,EAC1D,MAAM,EAAE,IAAI,CAAC,8BAA8B,EAAE,SAAS,GAAG,eAAe,GAAG,MAAM,CAAC,WAanF;AAED,eAAO,MAAM,sBAAsB,8DAKzB,CAAA;AACV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAA;AAExE,eAAO,MAAM,mBAAmB,oFAQtB,CAAA;AACV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,KAAK,oBAAoB,GAAG,IAAI,CAC9B,8BAA8B,EAC9B,SAAS,GAAG,wBAAwB,GAAG,eAAe,GAAG,MAAM,CAChE,CAAA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EAClE,MAAM,EAAE,oBAAoB,WAS7B;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IACL,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,kBAAkB,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB,EACD,MAAM,EAAE,oBAAoB,WAkB7B;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE;IACL,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,gBAAgB,CAAA;CAC5B,EACD,MAAM,EAAE,oBAAoB,WAS7B;AAwCD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,IAAI,CAAC,8BAA8B,EAAE,SAAS,GAAG,eAAe,GAAG,MAAM,CAAC,GAAG;IACnF,QAAQ,EAAE,yBAAyB,CAAA;CACpC,WAqBF;AAMD,KAAK,aAAa,GAAG,cAAc,GAAG,eAAe,CAAA;AAKrD;;;;;;;;;GASG;AACH,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IACzD;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAA;CAC9D;AA2HD,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,0BAA0B,CAAA;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B;AAED,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,mBAAmB,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IACpE,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,mBAAmB,CAAC,EAAE,+BAA+B,CAAA;CACtD;AAmDD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,yBAAyB,CAAA;IACnC,YAAY,EAAE,oCAAoC,CAAA;IAClD,6EAA6E;IAC7E,aAAa,EAAE,SAAS,6BAA6B,EAAE,CAAA;IACvD,iFAAiF;IACjF,KAAK,CAAC,EAAE,qBAAqB,CAAA;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAAA;IAC5C,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAA;IAC3C,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAA;IAC/C,6EAA6E;IAC7E,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,0BAA0B,CAAA;IACxD,mBAAmB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,kBAAkB,CAAA;KAAE,GAAG,OAAO,CAAA;IACxF,kBAAkB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAA;IACxE,kBAAkB,CAAC,KAAK,EAAE;QACxB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,EAAE,kBAAkB,CAAA;QACzB,UAAU,EAAE,MAAM,CAAA;KACnB,GAAG,OAAO,CAAA;IACX,iBAAiB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAA;KAAE,GAAG,OAAO,CAAA;IACnG,oGAAoG;IACpG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAA;CACxE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,8BAA8B,GAAG,iBAAiB,CAqElG"}
package/dist/analytics.js CHANGED
@@ -1,6 +1,7 @@
1
- 'use client';
2
- import { defineAnalyticsContract, normalizeEnhancedConversionEmail, normalizeEnhancedConversionPhone, sendBrandlabAnalyticsEvent, sendContactFormConversion, } from '@brandlab-digital/site-core/analytics';
3
- import { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes';
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