@jmp-technologies/analytics 0.1.27 → 0.1.32

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 (100) hide show
  1. package/dist/astro/client.cjs +256 -11
  2. package/dist/astro/client.cjs.map +1 -1
  3. package/dist/astro/client.d.cts +2 -2
  4. package/dist/astro/client.d.ts +2 -2
  5. package/dist/astro/client.js +2 -2
  6. package/dist/astro/content.cjs +308 -13
  7. package/dist/astro/content.cjs.map +1 -1
  8. package/dist/astro/content.d.cts +12 -3
  9. package/dist/astro/content.d.ts +12 -3
  10. package/dist/astro/content.js +9 -3
  11. package/dist/astro/integration.cjs +228 -10
  12. package/dist/astro/integration.cjs.map +1 -1
  13. package/dist/astro/integration.js +3 -3
  14. package/dist/astro/middleware.cjs +2 -1
  15. package/dist/astro/middleware.cjs.map +1 -1
  16. package/dist/astro/middleware.d.cts +2 -2
  17. package/dist/astro/middleware.d.ts +2 -2
  18. package/dist/astro/middleware.js +2 -2
  19. package/dist/astro/register.cjs +228 -10
  20. package/dist/astro/register.cjs.map +1 -1
  21. package/dist/astro/register.js +3 -3
  22. package/dist/{chunk-KZ4WVDGL.js → chunk-7623G3XL.js} +3 -3
  23. package/dist/{chunk-A4RRTY5D.js → chunk-AIFJ7DVO.js} +3 -2
  24. package/dist/{chunk-A4RRTY5D.js.map → chunk-AIFJ7DVO.js.map} +1 -1
  25. package/dist/{chunk-YCVHRMM6.js → chunk-G5BMWPQ6.js} +2 -3
  26. package/dist/{chunk-YCVHRMM6.js.map → chunk-G5BMWPQ6.js.map} +1 -1
  27. package/dist/{chunk-HODG4NJQ.js → chunk-HFLU7HQE.js} +2 -2
  28. package/dist/{chunk-PG3IOI5O.js → chunk-HKMDWTA2.js} +53 -6
  29. package/dist/chunk-HKMDWTA2.js.map +1 -0
  30. package/dist/{chunk-DNYJF7BG.js → chunk-KPTMZQCY.js} +3 -3
  31. package/dist/{chunk-FNWE2DT5.js → chunk-NDSPSAWQ.js} +18 -8
  32. package/dist/chunk-NDSPSAWQ.js.map +1 -0
  33. package/dist/{chunk-UVYF33XE.js → chunk-NI7R6QZQ.js} +293 -13
  34. package/dist/chunk-NI7R6QZQ.js.map +1 -0
  35. package/dist/chunk-RI3KKDPX.js +75 -0
  36. package/dist/chunk-RI3KKDPX.js.map +1 -0
  37. package/dist/{create-jmp-analytics-D4Qw_d6o.d.cts → create-jmp-analytics-D1dUsqCQ.d.cts} +1 -2
  38. package/dist/{create-jmp-analytics-DpG5Hdl5.d.ts → create-jmp-analytics-htx2oOEf.d.ts} +1 -2
  39. package/dist/index.cjs +295 -12
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +8 -6
  42. package/dist/index.d.ts +8 -6
  43. package/dist/index.js +10 -2
  44. package/dist/next/content.cjs +310 -13
  45. package/dist/next/content.cjs.map +1 -1
  46. package/dist/next/content.d.cts +7 -5
  47. package/dist/next/content.d.ts +7 -5
  48. package/dist/next/content.js +11 -3
  49. package/dist/next/content.js.map +1 -1
  50. package/dist/next/instrumentation.cjs +228 -10
  51. package/dist/next/instrumentation.cjs.map +1 -1
  52. package/dist/next/instrumentation.js +2 -2
  53. package/dist/next/landing-capture-script-server.cjs +1 -2
  54. package/dist/next/landing-capture-script-server.cjs.map +1 -1
  55. package/dist/next/landing-capture-script-server.js +1 -1
  56. package/dist/next/landing-pages.cjs +228 -12
  57. package/dist/next/landing-pages.cjs.map +1 -1
  58. package/dist/next/landing-pages.d.cts +4 -16
  59. package/dist/next/landing-pages.d.ts +4 -16
  60. package/dist/next/landing-pages.js +13 -67
  61. package/dist/next/landing-pages.js.map +1 -1
  62. package/dist/next/locations.cjs +737 -0
  63. package/dist/next/locations.cjs.map +1 -0
  64. package/dist/next/locations.d.cts +55 -0
  65. package/dist/next/locations.d.ts +55 -0
  66. package/dist/next/locations.js +158 -0
  67. package/dist/next/locations.js.map +1 -0
  68. package/dist/next/middleware.cjs +2 -1
  69. package/dist/next/middleware.cjs.map +1 -1
  70. package/dist/next/middleware.d.cts +2 -2
  71. package/dist/next/middleware.d.ts +2 -2
  72. package/dist/next/middleware.js +2 -2
  73. package/dist/next/site-root-server.cjs +259 -14
  74. package/dist/next/site-root-server.cjs.map +1 -1
  75. package/dist/next/site-root-server.js +5 -5
  76. package/dist/next.cjs +270 -17
  77. package/dist/next.cjs.map +1 -1
  78. package/dist/next.d.cts +3 -3
  79. package/dist/next.d.ts +3 -3
  80. package/dist/next.js +4 -4
  81. package/dist/react.cjs +389 -136
  82. package/dist/react.cjs.map +1 -1
  83. package/dist/react.d.cts +2 -2
  84. package/dist/react.d.ts +2 -2
  85. package/dist/react.js +3 -3
  86. package/dist/{site-config-UTzDG-UG.d.ts → site-config-B7USGGzu.d.cts} +22 -2
  87. package/dist/{site-config-CJqYnZm7.d.cts → site-config-C4bcUoLI.d.ts} +22 -2
  88. package/dist/{track-page-view-tX0Magkd.d.cts → track-page-view-C4TbbULE.d.cts} +1 -1
  89. package/dist/{track-page-view-CMAqyhD_.d.ts → track-page-view-RiB-xgIx.d.ts} +1 -1
  90. package/dist/{types-B-111Wht.d.ts → types-DRljY1Yy.d.cts} +92 -2
  91. package/dist/{types-B-111Wht.d.cts → types-DRljY1Yy.d.ts} +92 -2
  92. package/dist/utils-BajG58pj.d.cts +15 -0
  93. package/dist/utils-CHT3SQFv.d.ts +15 -0
  94. package/package.json +7 -2
  95. package/dist/chunk-FNWE2DT5.js.map +0 -1
  96. package/dist/chunk-PG3IOI5O.js.map +0 -1
  97. package/dist/chunk-UVYF33XE.js.map +0 -1
  98. /package/dist/{chunk-KZ4WVDGL.js.map → chunk-7623G3XL.js.map} +0 -0
  99. /package/dist/{chunk-HODG4NJQ.js.map → chunk-HFLU7HQE.js.map} +0 -0
  100. /package/dist/{chunk-DNYJF7BG.js.map → chunk-KPTMZQCY.js.map} +0 -0
package/dist/react.cjs CHANGED
@@ -28,6 +28,145 @@ __export(react_exports, {
28
28
  module.exports = __toCommonJS(react_exports);
29
29
  var import_react = require("react");
30
30
 
31
+ // src/contact-link-classifier.ts
32
+ var BOOKING_LINK_HOSTS = [
33
+ "calendly.com",
34
+ "cal.com",
35
+ "acuityscheduling.com",
36
+ "calendar.google.com",
37
+ "calendar.app.google",
38
+ "bookings.microsoft.com",
39
+ "outlook.office.com",
40
+ "outlook.office365.com",
41
+ "book.ms",
42
+ "setmore.com",
43
+ "simplybook.me",
44
+ "oncehub.com",
45
+ "scheduleonce.com",
46
+ "youcanbook.me",
47
+ "savvycal.com",
48
+ "tidycal.com",
49
+ "zohobookings.com",
50
+ "square.site",
51
+ "squareup.com",
52
+ "meetings.hubspot.com",
53
+ "jobber.com",
54
+ "housecallpro.com",
55
+ "servicetitan.com",
56
+ "thryv.com",
57
+ "honeybook.com",
58
+ "dubsado.com",
59
+ "fresha.com",
60
+ "booksy.com",
61
+ "vagaro.com",
62
+ "mindbodyonline.com",
63
+ "mindbody.io",
64
+ "janeapp.com",
65
+ "cliniko.com",
66
+ "practicebetter.io"
67
+ ];
68
+ var WHATSAPP_HOSTS = ["wa.me", "api.whatsapp.com", "whatsapp.com"];
69
+ function hostnameMatches(host, pattern) {
70
+ const h = host.toLowerCase();
71
+ const p = pattern.toLowerCase();
72
+ return h === p || h.endsWith(`.${p}`);
73
+ }
74
+ function isMapsHost(host, pathname) {
75
+ if (hostnameMatches(host, "maps.apple.com")) {
76
+ return true;
77
+ }
78
+ if (hostnameMatches(host, "maps.app.goo.gl")) {
79
+ return true;
80
+ }
81
+ if (hostnameMatches(host, "google.com") || hostnameMatches(host, "google.co")) {
82
+ return pathname === "/maps" || pathname.startsWith("/maps/") || pathname.startsWith("/local");
83
+ }
84
+ return false;
85
+ }
86
+ function isWhatsAppHost(host, pathname) {
87
+ for (const pattern of WHATSAPP_HOSTS) {
88
+ if (!hostnameMatches(host, pattern)) {
89
+ continue;
90
+ }
91
+ if (pattern === "whatsapp.com") {
92
+ return pathname.startsWith("/send");
93
+ }
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ function isBookingHost(host) {
99
+ for (const pattern of BOOKING_LINK_HOSTS) {
100
+ if (hostnameMatches(host, pattern)) {
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ }
106
+ function classifyContactHref(href) {
107
+ const raw = href.trim();
108
+ if (!raw || raw.startsWith("#") || raw.startsWith("javascript:")) {
109
+ return null;
110
+ }
111
+ const lower = raw.toLowerCase();
112
+ if (lower.startsWith("tel:")) {
113
+ return { kind: "call" };
114
+ }
115
+ if (lower.startsWith("mailto:")) {
116
+ return { kind: "email" };
117
+ }
118
+ if (lower.startsWith("sms:") || lower.startsWith("smsto:")) {
119
+ return { kind: "contact_click", channel: "sms", linkHost: null };
120
+ }
121
+ if (lower.startsWith("geo:")) {
122
+ return { kind: "contact_click", channel: "maps", linkHost: null };
123
+ }
124
+ if (raw.startsWith("/") || raw.startsWith("?")) {
125
+ return null;
126
+ }
127
+ let url;
128
+ try {
129
+ url = new URL(raw);
130
+ } catch {
131
+ return null;
132
+ }
133
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
134
+ return null;
135
+ }
136
+ const host = url.hostname.toLowerCase();
137
+ const pathname = url.pathname.toLowerCase();
138
+ if (isWhatsAppHost(host, pathname)) {
139
+ return { kind: "contact_click", channel: "whatsapp", linkHost: host };
140
+ }
141
+ if (isMapsHost(host, pathname)) {
142
+ return { kind: "contact_click", channel: "maps", linkHost: host };
143
+ }
144
+ if (isBookingHost(host)) {
145
+ return { kind: "contact_click", channel: "booking", linkHost: host };
146
+ }
147
+ return null;
148
+ }
149
+ function extractTelTarget(href) {
150
+ const raw = href.trim();
151
+ if (!raw.toLowerCase().startsWith("tel:")) {
152
+ return null;
153
+ }
154
+ const target = decodeURIComponent(
155
+ raw.slice(4).split("?")[0]?.split(";")[0] ?? ""
156
+ ).trim();
157
+ return target || null;
158
+ }
159
+ function extractMailtoTarget(href) {
160
+ const raw = href.trim();
161
+ if (!raw.toLowerCase().startsWith("mailto:")) {
162
+ return null;
163
+ }
164
+ const target = decodeURIComponent(
165
+ raw.slice(7).split("?")[0]?.split(",")[0] ?? ""
166
+ ).trim();
167
+ return target || null;
168
+ }
169
+
31
170
  // src/landing-capture.ts
32
171
  var JMP_SESSION_ID_KEY = "jmp_analytics_sid";
33
172
  var JMP_LANDING_REFERER_KEY = "jmp_analytics_landing_referer";
@@ -166,125 +305,6 @@ function getOrCreateSessionIdFromStorage() {
166
305
  }
167
306
  }
168
307
 
169
- // src/contact-link-classifier.ts
170
- var BOOKING_LINK_HOSTS = [
171
- "calendly.com",
172
- "cal.com",
173
- "acuityscheduling.com",
174
- "calendar.google.com",
175
- "calendar.app.google",
176
- "bookings.microsoft.com",
177
- "outlook.office.com",
178
- "outlook.office365.com",
179
- "book.ms",
180
- "setmore.com",
181
- "simplybook.me",
182
- "oncehub.com",
183
- "scheduleonce.com",
184
- "youcanbook.me",
185
- "savvycal.com",
186
- "tidycal.com",
187
- "zohobookings.com",
188
- "square.site",
189
- "squareup.com",
190
- "meetings.hubspot.com",
191
- "jobber.com",
192
- "housecallpro.com",
193
- "servicetitan.com",
194
- "thryv.com",
195
- "honeybook.com",
196
- "dubsado.com",
197
- "fresha.com",
198
- "booksy.com",
199
- "vagaro.com",
200
- "mindbodyonline.com",
201
- "mindbody.io",
202
- "janeapp.com",
203
- "cliniko.com",
204
- "practicebetter.io"
205
- ];
206
- var WHATSAPP_HOSTS = ["wa.me", "api.whatsapp.com", "whatsapp.com"];
207
- function hostnameMatches(host, pattern) {
208
- const h = host.toLowerCase();
209
- const p = pattern.toLowerCase();
210
- return h === p || h.endsWith(`.${p}`);
211
- }
212
- function isMapsHost(host, pathname) {
213
- if (hostnameMatches(host, "maps.apple.com")) {
214
- return true;
215
- }
216
- if (hostnameMatches(host, "maps.app.goo.gl")) {
217
- return true;
218
- }
219
- if (hostnameMatches(host, "google.com") || hostnameMatches(host, "google.co")) {
220
- return pathname === "/maps" || pathname.startsWith("/maps/") || pathname.startsWith("/local");
221
- }
222
- return false;
223
- }
224
- function isWhatsAppHost(host, pathname) {
225
- for (const pattern of WHATSAPP_HOSTS) {
226
- if (!hostnameMatches(host, pattern)) {
227
- continue;
228
- }
229
- if (pattern === "whatsapp.com") {
230
- return pathname.startsWith("/send");
231
- }
232
- return true;
233
- }
234
- return false;
235
- }
236
- function isBookingHost(host) {
237
- for (const pattern of BOOKING_LINK_HOSTS) {
238
- if (hostnameMatches(host, pattern)) {
239
- return true;
240
- }
241
- }
242
- return false;
243
- }
244
- function classifyContactHref(href) {
245
- const raw = href.trim();
246
- if (!raw || raw.startsWith("#") || raw.startsWith("javascript:")) {
247
- return null;
248
- }
249
- const lower = raw.toLowerCase();
250
- if (lower.startsWith("tel:")) {
251
- return { kind: "call" };
252
- }
253
- if (lower.startsWith("mailto:")) {
254
- return { kind: "email" };
255
- }
256
- if (lower.startsWith("sms:") || lower.startsWith("smsto:")) {
257
- return { kind: "contact_click", channel: "sms", linkHost: null };
258
- }
259
- if (lower.startsWith("geo:")) {
260
- return { kind: "contact_click", channel: "maps", linkHost: null };
261
- }
262
- if (raw.startsWith("/") || raw.startsWith("?")) {
263
- return null;
264
- }
265
- let url;
266
- try {
267
- url = new URL(raw);
268
- } catch {
269
- return null;
270
- }
271
- if (url.protocol !== "http:" && url.protocol !== "https:") {
272
- return null;
273
- }
274
- const host = url.hostname.toLowerCase();
275
- const pathname = url.pathname.toLowerCase();
276
- if (isWhatsAppHost(host, pathname)) {
277
- return { kind: "contact_click", channel: "whatsapp", linkHost: host };
278
- }
279
- if (isMapsHost(host, pathname)) {
280
- return { kind: "contact_click", channel: "maps", linkHost: host };
281
- }
282
- if (isBookingHost(host)) {
283
- return { kind: "contact_click", channel: "booking", linkHost: host };
284
- }
285
- return null;
286
- }
287
-
288
308
  // src/constants.ts
289
309
  var JMP_ANALYTICS_SDK_VERSION = "0.1.26";
290
310
  var JMP_TRACKING_CONSENT_STORAGE_KEY = "jmp_analytics_consent_v1";
@@ -700,14 +720,22 @@ function createJmpAnalytics(config) {
700
720
  if (dedupeContactClick(`call_click:${path}`)) {
701
721
  return;
702
722
  }
703
- void trackCallClick(path);
723
+ const contactTarget = extractTelTarget(href);
724
+ void trackCallClick(
725
+ path,
726
+ contactTarget ? { contactTarget } : void 0
727
+ );
704
728
  return;
705
729
  }
706
730
  if (classified.kind === "email") {
707
731
  if (dedupeContactClick(`email_click:${path}`)) {
708
732
  return;
709
733
  }
710
- void trackEmailClick(path);
734
+ const contactTarget = extractMailtoTarget(href);
735
+ void trackEmailClick(
736
+ path,
737
+ contactTarget ? { contactTarget } : void 0
738
+ );
711
739
  return;
712
740
  }
713
741
  if (dedupeContactClick(`contact_click:${classified.channel}:${path}`)) {
@@ -753,6 +781,209 @@ function createJmpAnalytics(config) {
753
781
  };
754
782
  }
755
783
 
784
+ // src/content/manifest.ts
785
+ var JMP_CONTENT_MANIFEST_VERSION = 1;
786
+ var TRUST_SECTIONS = /* @__PURE__ */ new Set([
787
+ "summary",
788
+ "testimonials",
789
+ "reviews",
790
+ "stats",
791
+ "logos",
792
+ "credentials"
793
+ ]);
794
+ function normalizeKey(value) {
795
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
796
+ }
797
+ function isValidExtraKey(key) {
798
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key);
799
+ }
800
+ var MAX_EXTERNAL_ADMIN_LINKS = 10;
801
+ var MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH = 80;
802
+ var EXTERNAL_ADMIN_LINK_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
803
+ function tryNormalizeHttpUrl(input) {
804
+ if (typeof input !== "string" || !input.trim()) {
805
+ return null;
806
+ }
807
+ try {
808
+ const url = new URL(input.trim());
809
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
810
+ return null;
811
+ }
812
+ return url.toString().replace(/\/$/, "");
813
+ } catch {
814
+ return null;
815
+ }
816
+ }
817
+ function normalizeShopifyExternalSource(shopify) {
818
+ if (!shopify || typeof shopify !== "object") {
819
+ return null;
820
+ }
821
+ const adminUrl = tryNormalizeHttpUrl(shopify.adminUrl);
822
+ const storeUrl = tryNormalizeHttpUrl(shopify.storeUrl);
823
+ if (!adminUrl && !storeUrl) {
824
+ return null;
825
+ }
826
+ return {
827
+ ...adminUrl ? { adminUrl } : {},
828
+ ...storeUrl ? { storeUrl } : {}
829
+ };
830
+ }
831
+ function normalizeExternalAdminLinkEntries(links) {
832
+ if (!Array.isArray(links)) {
833
+ return [];
834
+ }
835
+ const seenIds = /* @__PURE__ */ new Set();
836
+ const out = [];
837
+ for (const raw of links) {
838
+ if (!raw || typeof raw !== "object" || out.length >= MAX_EXTERNAL_ADMIN_LINKS) {
839
+ continue;
840
+ }
841
+ const id = typeof raw.id === "string" ? raw.id.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") : "";
842
+ const label = typeof raw.label === "string" ? raw.label.trim().slice(0, MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH) : "";
843
+ const url = tryNormalizeHttpUrl(raw.url);
844
+ if (!id || !label || !url || !EXTERNAL_ADMIN_LINK_ID_PATTERN.test(id) || seenIds.has(id)) {
845
+ continue;
846
+ }
847
+ seenIds.add(id);
848
+ out.push({ id, label, url });
849
+ }
850
+ return out;
851
+ }
852
+ function normalizeExternalSources(externalSources) {
853
+ if (!externalSources || typeof externalSources !== "object") {
854
+ return null;
855
+ }
856
+ const studioUrl = tryNormalizeHttpUrl(externalSources.sanity?.studioUrl);
857
+ const shopify = normalizeShopifyExternalSource(externalSources.shopify);
858
+ const links = normalizeExternalAdminLinkEntries(externalSources.links);
859
+ if (!studioUrl && !shopify && links.length === 0) {
860
+ return null;
861
+ }
862
+ return {
863
+ ...studioUrl ? { sanity: { studioUrl } } : {},
864
+ ...shopify ? { shopify } : {},
865
+ ...links.length > 0 ? { links } : {}
866
+ };
867
+ }
868
+ function normalizeJmpContentManifest(input) {
869
+ if (!input || typeof input !== "object") {
870
+ return null;
871
+ }
872
+ const routes = normalizeRoutes(input.routes);
873
+ const externalSources = normalizeExternalSources(input.externalSources);
874
+ const trust = normalizeTrust(input.trust);
875
+ const contact = normalizeContact(input.contact);
876
+ if (routes.length === 0 && !externalSources && !trust && !contact) {
877
+ return null;
878
+ }
879
+ return {
880
+ version: JMP_CONTENT_MANIFEST_VERSION,
881
+ ...routes.length > 0 ? { routes } : {},
882
+ ...externalSources ? { externalSources } : {},
883
+ ...trust ? { trust } : {},
884
+ ...contact ? { contact } : {}
885
+ };
886
+ }
887
+ function normalizeRoutes(routes) {
888
+ if (!Array.isArray(routes)) {
889
+ return [];
890
+ }
891
+ const seen = /* @__PURE__ */ new Set();
892
+ const out = [];
893
+ for (const raw of routes) {
894
+ if (!raw || typeof raw !== "object") continue;
895
+ const wrapper = typeof raw.wrapper === "string" ? raw.wrapper.trim().toLowerCase() : "";
896
+ const pattern = typeof raw.pattern === "string" ? raw.pattern.trim() : "";
897
+ if (!wrapper || !pattern) continue;
898
+ const dedupe = `${wrapper}:${pattern}`;
899
+ if (seen.has(dedupe)) continue;
900
+ seen.add(dedupe);
901
+ out.push({
902
+ wrapper,
903
+ pattern,
904
+ ...typeof raw.index === "string" && raw.index.trim() ? { index: raw.index.trim() } : {},
905
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
906
+ });
907
+ }
908
+ return out;
909
+ }
910
+ function normalizeTrust(trust) {
911
+ if (!trust || typeof trust !== "object") return null;
912
+ const blocks = normalizeTrustBlocks(trust.blocks);
913
+ const labels = normalizeTrustLabels(trust.labels);
914
+ const mergeTestimonialsReviews = trust.mergeTestimonialsReviews === true;
915
+ if (blocks.length === 0 && !labels && !mergeTestimonialsReviews) return null;
916
+ return {
917
+ ...blocks.length > 0 ? { blocks } : {},
918
+ ...labels ? { labels } : {},
919
+ ...mergeTestimonialsReviews ? { mergeTestimonialsReviews: true } : {}
920
+ };
921
+ }
922
+ function normalizeTrustBlocks(blocks) {
923
+ if (!Array.isArray(blocks)) return [];
924
+ const seen = /* @__PURE__ */ new Set();
925
+ const out = [];
926
+ for (const raw of blocks) {
927
+ if (!raw || typeof raw !== "object") continue;
928
+ const section = typeof raw.section === "string" ? raw.section.trim().toLowerCase() : "";
929
+ if (!TRUST_SECTIONS.has(section) || seen.has(section)) continue;
930
+ seen.add(section);
931
+ out.push({
932
+ section,
933
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
934
+ });
935
+ }
936
+ return out;
937
+ }
938
+ function normalizeTrustLabels(labels) {
939
+ if (!labels || typeof labels !== "object") return null;
940
+ const out = {};
941
+ for (const [key, value] of Object.entries(labels)) {
942
+ if (TRUST_SECTIONS.has(key) && typeof value === "string" && value.trim()) {
943
+ out[key] = value.trim();
944
+ }
945
+ }
946
+ return Object.keys(out).length > 0 ? out : null;
947
+ }
948
+ function normalizeContact(contact) {
949
+ if (!contact || typeof contact !== "object") return null;
950
+ const extras = normalizeContactExtras(contact.extras);
951
+ return extras.length > 0 ? { extras } : null;
952
+ }
953
+ function normalizeContactExtras(extras) {
954
+ if (!Array.isArray(extras)) return [];
955
+ const seen = /* @__PURE__ */ new Set();
956
+ const out = [];
957
+ for (const raw of extras) {
958
+ if (!raw || typeof raw !== "object") continue;
959
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
960
+ const key = normalizeKey(
961
+ typeof raw.key === "string" && raw.key.trim() ? raw.key : label
962
+ );
963
+ if (!key || !label || !isValidExtraKey(key) || seen.has(key)) continue;
964
+ seen.add(key);
965
+ out.push({ key, label });
966
+ }
967
+ return out;
968
+ }
969
+ function stableJmpManifestFingerprint(manifest) {
970
+ return manifest ? JSON.stringify(manifest) : "";
971
+ }
972
+ function inferWrapperSlotsFromManifest(manifest) {
973
+ const slots = /* @__PURE__ */ new Set();
974
+ for (const route of manifest?.routes ?? []) {
975
+ slots.add(route.wrapper);
976
+ }
977
+ for (const block of manifest?.trust?.blocks ?? []) {
978
+ slots.add(`trust.${block.section}`);
979
+ slots.add("trust");
980
+ }
981
+ if (manifest?.contact?.extras?.length) {
982
+ slots.add("contact");
983
+ }
984
+ return [...slots];
985
+ }
986
+
756
987
  // src/content/wrapper-discovery.ts
757
988
  var WRAPPER_SLOT_ORDER = [
758
989
  "blog",
@@ -772,16 +1003,22 @@ var syncHandler = null;
772
1003
  function getDiscoveredContentWrappers() {
773
1004
  return sortWrapperSlots([...discovered]);
774
1005
  }
1006
+ function resolveContentManifestForReport(config) {
1007
+ return normalizeJmpContentManifest(config.contentManifest);
1008
+ }
775
1009
  function resolveContentWrappersForReport(config) {
776
1010
  const explicit = config.contentWrappers ?? [];
777
1011
  const auto = getDiscoveredContentWrappers();
1012
+ const fromManifest = inferWrapperSlotsFromManifest(
1013
+ resolveContentManifestForReport(config)
1014
+ );
778
1015
  if (config.contentWrapperMode === "manual") {
779
- return sortWrapperSlots([...explicit]);
1016
+ return sortWrapperSlots([...explicit, ...fromManifest]);
780
1017
  }
781
1018
  if (config.contentWrapperMode === "explicit-only") {
782
- return sortWrapperSlots([...explicit]);
1019
+ return sortWrapperSlots([...explicit, ...fromManifest]);
783
1020
  }
784
- const merged = /* @__PURE__ */ new Set([...explicit, ...auto]);
1021
+ const merged = /* @__PURE__ */ new Set([...explicit, ...auto, ...fromManifest]);
785
1022
  return sortWrapperSlots([...merged]);
786
1023
  }
787
1024
  function hasReportedContentWrappers(dedupeKey) {
@@ -811,7 +1048,8 @@ async function registerJmpContentWrappers(config) {
811
1048
  if (!wrappers.length) {
812
1049
  return { ok: false, skipped: true };
813
1050
  }
814
- const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}`;
1051
+ const manifest = resolveContentManifestForReport(config);
1052
+ const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}:${stableJmpManifestFingerprint(manifest)}`;
815
1053
  if (hasReportedContentWrappers(dedupeKey)) {
816
1054
  return { ok: true, wrappers };
817
1055
  }
@@ -824,7 +1062,17 @@ async function registerJmpContentWrappers(config) {
824
1062
  }
825
1063
  const url = `${baseUrl}/api/content/capabilities`;
826
1064
  if (config.debug) {
827
- console.debug("[@jmp-technologies/analytics]", "POST", url, { wrappers });
1065
+ console.debug("[@jmp-technologies/analytics]", "POST", url, {
1066
+ wrappers,
1067
+ manifest
1068
+ });
1069
+ }
1070
+ const body = {
1071
+ wrappers,
1072
+ sdkVersion: JMP_ANALYTICS_SDK_VERSION
1073
+ };
1074
+ if (manifest) {
1075
+ body.manifest = manifest;
828
1076
  }
829
1077
  const res = await resolvedFetch(url, {
830
1078
  method: "POST",
@@ -832,10 +1080,7 @@ async function registerJmpContentWrappers(config) {
832
1080
  "Content-Type": "application/json",
833
1081
  "X-JMP-Tracking-Key": config.trackingKey
834
1082
  },
835
- body: JSON.stringify({
836
- wrappers,
837
- sdkVersion: JMP_ANALYTICS_SDK_VERSION
838
- })
1083
+ body: JSON.stringify(body)
839
1084
  });
840
1085
  const text = await res.text();
841
1086
  if (!res.ok) {
@@ -937,13 +1182,21 @@ var import_jsx_runtime = require("react/jsx-runtime");
937
1182
  function useJmpAnalytics() {
938
1183
  return (0, import_react.useMemo)(() => getOrCreateJmpAnalyticsFromBrowserEnv(), []);
939
1184
  }
940
- function trackCallClickFromCurrentPath() {
1185
+ function trackCallClickFromHref(href) {
941
1186
  const path = typeof window !== "undefined" ? window.location.pathname : "/";
942
- void getOrCreateJmpAnalyticsFromBrowserEnv()?.trackCallClick(path);
1187
+ const contactTarget = extractTelTarget(href);
1188
+ void getOrCreateJmpAnalyticsFromBrowserEnv()?.trackCallClick(
1189
+ path,
1190
+ contactTarget ? { contactTarget } : void 0
1191
+ );
943
1192
  }
944
- function trackEmailClickFromCurrentPath() {
1193
+ function trackEmailClickFromHref(href) {
945
1194
  const path = typeof window !== "undefined" ? window.location.pathname : "/";
946
- void getOrCreateJmpAnalyticsFromBrowserEnv()?.trackEmailClick(path);
1195
+ const contactTarget = extractMailtoTarget(href);
1196
+ void getOrCreateJmpAnalyticsFromBrowserEnv()?.trackEmailClick(
1197
+ path,
1198
+ contactTarget ? { contactTarget } : void 0
1199
+ );
947
1200
  }
948
1201
  function JmpTrackedTelLink({
949
1202
  href,
@@ -957,7 +1210,7 @@ function JmpTrackedTelLink({
957
1210
  "data-jmp-tracked": "true",
958
1211
  ...rest,
959
1212
  onClick: (e) => {
960
- trackCallClickFromCurrentPath();
1213
+ trackCallClickFromHref(href);
961
1214
  onClick?.(e);
962
1215
  }
963
1216
  }
@@ -975,7 +1228,7 @@ function JmpTrackedMailtoLink({
975
1228
  "data-jmp-tracked": "true",
976
1229
  ...rest,
977
1230
  onClick: (e) => {
978
- trackEmailClickFromCurrentPath();
1231
+ trackEmailClickFromHref(href);
979
1232
  onClick?.(e);
980
1233
  }
981
1234
  }