@jmp-technologies/analytics 0.1.17 → 0.1.19

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 (40) hide show
  1. package/README.md +3 -1
  2. package/dist/{chunk-3ELQETAA.js → chunk-EPNF76VK.js} +2 -2
  3. package/dist/{chunk-MUHLIN7P.js → chunk-GB37RVJJ.js} +339 -103
  4. package/dist/chunk-GB37RVJJ.js.map +1 -0
  5. package/dist/index.cjs +348 -104
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +112 -35
  8. package/dist/index.d.ts +112 -35
  9. package/dist/index.js +21 -5
  10. package/dist/next/content.cjs +322 -0
  11. package/dist/next/content.cjs.map +1 -0
  12. package/dist/next/content.d.cts +37 -0
  13. package/dist/next/content.d.ts +37 -0
  14. package/dist/next/content.js +172 -0
  15. package/dist/next/content.js.map +1 -0
  16. package/dist/next/instrumentation.cjs +131 -0
  17. package/dist/next/instrumentation.cjs.map +1 -0
  18. package/dist/next/instrumentation.d.cts +7 -0
  19. package/dist/next/instrumentation.d.ts +7 -0
  20. package/dist/next/instrumentation.js +18 -0
  21. package/dist/next/instrumentation.js.map +1 -0
  22. package/dist/next/middleware.cjs +147 -0
  23. package/dist/next/middleware.cjs.map +1 -0
  24. package/dist/next/middleware.d.cts +13 -0
  25. package/dist/next/middleware.d.ts +13 -0
  26. package/dist/next/middleware.js +22 -0
  27. package/dist/next/middleware.js.map +1 -0
  28. package/dist/next.cjs +322 -86
  29. package/dist/next.cjs.map +1 -1
  30. package/dist/next.d.cts +24 -8
  31. package/dist/next.d.ts +24 -8
  32. package/dist/next.js +41 -13
  33. package/dist/next.js.map +1 -1
  34. package/dist/react.cjs +279 -69
  35. package/dist/react.cjs.map +1 -1
  36. package/dist/react.js +2 -2
  37. package/docs/legal-and-privacy.md +1 -0
  38. package/package.json +17 -2
  39. package/dist/chunk-MUHLIN7P.js.map +0 -1
  40. /package/dist/{chunk-3ELQETAA.js.map → chunk-EPNF76VK.js.map} +0 -0
package/dist/react.cjs CHANGED
@@ -33,7 +33,6 @@ var JMP_SESSION_ID_KEY = "jmp_analytics_sid";
33
33
  var JMP_LANDING_REFERER_KEY = "jmp_analytics_landing_referer";
34
34
  var JMP_LANDING_REFERER_COOKIE = "jmp_lr";
35
35
  var JMP_UTM_SESSION_KEY = "jmp_analytics_utm";
36
- var JMP_EARLY_BEACON_PATH_KEY = "jmp_analytics_early_beacon_path";
37
36
  var LANDING_REFERER_COOKIE_MAX_AGE_SEC = 30 * 60;
38
37
  var JMP_UTM_PARAM_KEYS = [
39
38
  "utm_source",
@@ -166,75 +165,164 @@ function getOrCreateSessionIdFromStorage() {
166
165
  return void 0;
167
166
  }
168
167
  }
169
- function wasEarlyBeaconSentForPath(path) {
170
- if (typeof window === "undefined") {
171
- return false;
168
+
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;
172
215
  }
173
- try {
174
- return sessionStorage.getItem(JMP_EARLY_BEACON_PATH_KEY) === path;
175
- } catch {
176
- return false;
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");
177
221
  }
222
+ return false;
178
223
  }
179
- function markEarlyBeaconSentForPath(path) {
180
- if (typeof window === "undefined") {
181
- return;
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 };
182
261
  }
262
+ if (raw.startsWith("/") || raw.startsWith("?")) {
263
+ return null;
264
+ }
265
+ let url;
183
266
  try {
184
- sessionStorage.setItem(JMP_EARLY_BEACON_PATH_KEY, path);
267
+ url = new URL(raw);
185
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 };
186
281
  }
282
+ if (isBookingHost(host)) {
283
+ return { kind: "contact_click", channel: "booking", linkHost: host };
284
+ }
285
+ return null;
187
286
  }
188
- function sendEarlyPageViewBeacon(args) {
189
- if (typeof window === "undefined" || typeof fetch === "undefined") {
287
+
288
+ // src/index.ts
289
+ var JMP_TRACKING_CONSENT_STORAGE_KEY = "jmp_analytics_consent_v1";
290
+ var moduleTrackingConsentGranted = true;
291
+ var trackingConsentListeners = /* @__PURE__ */ new Set();
292
+ function readStoredTrackingConsent() {
293
+ if (typeof window === "undefined") {
190
294
  return false;
191
295
  }
192
- const path = `${window.location.pathname}${window.location.search}` || "/";
193
- if (wasEarlyBeaconSentForPath(path)) {
296
+ try {
297
+ return localStorage.getItem(JMP_TRACKING_CONSENT_STORAGE_KEY) === "granted";
298
+ } catch {
194
299
  return false;
195
300
  }
196
- const { landingReferer, utm } = captureLandingAttribution(window.location.hostname);
197
- const sessionId = getOrCreateSessionIdFromStorage();
198
- const metadata = {
199
- pageHostname: window.location.hostname,
200
- earlyBeacon: true,
201
- ...utm
202
- };
203
- if (landingReferer) {
204
- metadata.referer = landingReferer;
205
- }
206
- const payload = {
207
- type: "page_view",
208
- path,
209
- trackingKey: args.trackingKey,
210
- ...sessionId ? { sessionId } : {},
211
- metadata
212
- };
213
- const headers = {
214
- "Content-Type": "application/json",
215
- "X-JMP-Tracking-Key": args.trackingKey
216
- };
217
- if (landingReferer) {
218
- headers["X-JMP-Visitor-Referer"] = landingReferer;
219
- }
220
- markEarlyBeaconSentForPath(path);
221
- void fetch(`${args.baseUrl.replace(/\/$/, "")}/api/events`, {
222
- method: "POST",
223
- headers,
224
- body: JSON.stringify(payload),
225
- keepalive: true,
226
- mode: "cors",
227
- credentials: "omit"
228
- }).catch(() => {
301
+ }
302
+ function isJmpTrackingConsentGranted() {
303
+ return moduleTrackingConsentGranted;
304
+ }
305
+ function setJmpTrackingConsent(granted) {
306
+ if (moduleTrackingConsentGranted === granted) {
307
+ return;
308
+ }
309
+ moduleTrackingConsentGranted = granted;
310
+ if (typeof window !== "undefined") {
229
311
  try {
230
- sessionStorage.removeItem(JMP_EARLY_BEACON_PATH_KEY);
312
+ localStorage.setItem(
313
+ JMP_TRACKING_CONSENT_STORAGE_KEY,
314
+ granted ? "granted" : "denied"
315
+ );
231
316
  } catch {
232
317
  }
233
- });
234
- return true;
318
+ if (granted) {
319
+ captureLandingAttribution(window.location.hostname);
320
+ }
321
+ }
322
+ for (const listener of trackingConsentListeners) {
323
+ listener();
324
+ }
235
325
  }
236
-
237
- // src/index.ts
238
326
  function captureAndGetLandingReferer() {
239
327
  if (typeof window === "undefined") {
240
328
  return null;
@@ -284,9 +372,16 @@ function createJmpAnalytics(config) {
284
372
  );
285
373
  }
286
374
  const fetchFn = resolvedFetch;
375
+ moduleTrackingConsentGranted = config.requireConsent ? readStoredTrackingConsent() : true;
376
+ function canSend() {
377
+ return moduleTrackingConsentGranted;
378
+ }
287
379
  let lastDedupePath = "";
288
380
  let lastDedupeAt = 0;
289
381
  async function send(body) {
382
+ if (!canSend()) {
383
+ return;
384
+ }
290
385
  const url = `${baseUrl}/api/events`;
291
386
  const headers = {
292
387
  "Content-Type": "application/json",
@@ -342,6 +437,9 @@ function createJmpAnalytics(config) {
342
437
  }
343
438
  }
344
439
  async function sendLead(body) {
440
+ if (!canSend()) {
441
+ return;
442
+ }
345
443
  const url = `${baseUrl}/api/leads`;
346
444
  const headers = {
347
445
  "Content-Type": "application/json",
@@ -404,6 +502,9 @@ function createJmpAnalytics(config) {
404
502
  return false;
405
503
  }
406
504
  function trackPageView(options) {
505
+ if (!canSend()) {
506
+ return Promise.resolve();
507
+ }
407
508
  const pathFromOptions = options?.path?.trim();
408
509
  let pathname = "/";
409
510
  let search = "";
@@ -419,9 +520,6 @@ function createJmpAnalytics(config) {
419
520
  search = window.location.search.replace(/^\?/, "");
420
521
  }
421
522
  const path = search ? `${pathname}?${search}` : pathname;
422
- if (wasEarlyBeaconSentForPath(path)) {
423
- return Promise.resolve();
424
- }
425
523
  if (shouldDedupePageView(pathname)) {
426
524
  return Promise.resolve();
427
525
  }
@@ -472,6 +570,20 @@ function createJmpAnalytics(config) {
472
570
  metadata: metadata ?? {}
473
571
  });
474
572
  }
573
+ function trackContactClick(path, options) {
574
+ const metadata = {
575
+ ...options.metadata && typeof options.metadata === "object" ? options.metadata : {},
576
+ channel: options.channel
577
+ };
578
+ if (options.linkHost) {
579
+ metadata.linkHost = options.linkHost;
580
+ }
581
+ return send({
582
+ type: "contact_click",
583
+ path: path || "/",
584
+ metadata
585
+ });
586
+ }
475
587
  function trackLead(options) {
476
588
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
477
589
  const body = { path: path || "/" };
@@ -504,7 +616,20 @@ function createJmpAnalytics(config) {
504
616
  if (type === "call_click") {
505
617
  return trackCallClick(payload.path, payload.metadata);
506
618
  }
507
- return trackEmailClick(payload.path, payload.metadata);
619
+ if (type === "email_click") {
620
+ return trackEmailClick(payload.path, payload.metadata);
621
+ }
622
+ const meta = payload.metadata ?? {};
623
+ const channel = typeof meta.channel === "string" ? meta.channel : "";
624
+ const linkHost = typeof meta.linkHost === "string" ? meta.linkHost : null;
625
+ if (channel !== "sms" && channel !== "whatsapp" && channel !== "maps" && channel !== "booking") {
626
+ return Promise.resolve();
627
+ }
628
+ return trackContactClick(payload.path, {
629
+ channel,
630
+ linkHost,
631
+ metadata: meta
632
+ });
508
633
  }
509
634
  function subscribeToSpaNavigation() {
510
635
  if (typeof window === "undefined") {
@@ -538,12 +663,75 @@ function createJmpAnalytics(config) {
538
663
  history.replaceState = replace;
539
664
  };
540
665
  }
541
- if (typeof window !== "undefined") {
666
+ let lastContactClickKey = "";
667
+ let lastContactClickAt = 0;
668
+ function subscribeContactLinkAutoTrack() {
669
+ if (typeof window === "undefined" || typeof document === "undefined") {
670
+ return () => {
671
+ };
672
+ }
673
+ const handler = (event) => {
674
+ if (!canSend()) {
675
+ return;
676
+ }
677
+ const target = event.target;
678
+ if (!(target instanceof Element)) {
679
+ return;
680
+ }
681
+ const anchor = target.closest("a[href]");
682
+ if (!(anchor instanceof HTMLAnchorElement)) {
683
+ return;
684
+ }
685
+ if (anchor.closest("[data-jmp-no-track]")) {
686
+ return;
687
+ }
688
+ const href = anchor.getAttribute("href") ?? "";
689
+ const classified = classifyContactHref(href);
690
+ if (!classified) {
691
+ return;
692
+ }
693
+ const path = window.location.pathname;
694
+ if (classified.kind === "call") {
695
+ if (dedupeContactClick(`call_click:${path}`)) {
696
+ return;
697
+ }
698
+ void trackCallClick(path);
699
+ return;
700
+ }
701
+ if (classified.kind === "email") {
702
+ if (dedupeContactClick(`email_click:${path}`)) {
703
+ return;
704
+ }
705
+ void trackEmailClick(path);
706
+ return;
707
+ }
708
+ if (dedupeContactClick(`contact_click:${classified.channel}:${path}`)) {
709
+ return;
710
+ }
711
+ void trackContactClick(path, {
712
+ channel: classified.channel,
713
+ linkHost: classified.linkHost
714
+ });
715
+ };
716
+ document.addEventListener("click", handler, { capture: true });
717
+ return () => {
718
+ document.removeEventListener("click", handler, { capture: true });
719
+ };
720
+ }
721
+ function dedupeContactClick(key) {
722
+ const now = Date.now();
723
+ if (key === lastContactClickKey && now - lastContactClickAt < 1e3) {
724
+ return true;
725
+ }
726
+ lastContactClickKey = key;
727
+ lastContactClickAt = now;
728
+ return false;
729
+ }
730
+ if (typeof window !== "undefined" && canSend()) {
542
731
  captureLandingAttribution(window.location.hostname);
543
- sendEarlyPageViewBeacon({
544
- baseUrl,
545
- trackingKey: config.trackingKey
546
- });
732
+ }
733
+ if (typeof window !== "undefined" && config.autoTrackContactLinks && canSend()) {
734
+ subscribeContactLinkAutoTrack();
547
735
  }
548
736
  return {
549
737
  track,
@@ -551,25 +739,47 @@ function createJmpAnalytics(config) {
551
739
  trackFormSubmit,
552
740
  trackCallClick,
553
741
  trackEmailClick,
742
+ trackContactClick,
554
743
  trackLead,
555
- subscribeToSpaNavigation
744
+ subscribeToSpaNavigation,
745
+ subscribeContactLinkAutoTrack,
746
+ setTrackingConsent: setJmpTrackingConsent,
747
+ isTrackingConsentGranted: isJmpTrackingConsentGranted
556
748
  };
557
749
  }
558
750
  var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
559
- function readProcessEnv(key) {
751
+ function readNextPublicEnv(key) {
560
752
  if (typeof process === "undefined" || !process.env) return void 0;
561
- const v = process.env[key];
753
+ let v;
754
+ switch (key) {
755
+ case "NEXT_PUBLIC_JMP_ANALYTICS_URL":
756
+ v = process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL;
757
+ break;
758
+ case "NEXT_PUBLIC_JMP_TRACKING_KEY":
759
+ v = process.env.NEXT_PUBLIC_JMP_TRACKING_KEY;
760
+ break;
761
+ case "NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT":
762
+ v = process.env.NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT;
763
+ break;
764
+ case "NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS":
765
+ v = process.env.NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS;
766
+ break;
767
+ }
562
768
  return typeof v === "string" && v.length > 0 ? v : void 0;
563
769
  }
564
770
  function getJmpBrowserAnalyticsConfigFromEnv(overrides) {
565
- const baseUrl = readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
566
- const trackingKey = readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
771
+ const baseUrl = readNextPublicEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
772
+ const trackingKey = readNextPublicEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
567
773
  if (!baseUrl || !trackingKey) return null;
774
+ const requireConsentFromEnv = readNextPublicEnv("NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT") === "true";
775
+ const autoTrackContactLinksFromEnv = readNextPublicEnv("NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS") === "true";
568
776
  return {
569
777
  baseUrl,
570
778
  trackingKey,
571
779
  pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
572
- ...overrides
780
+ ...overrides,
781
+ requireConsent: overrides?.requireConsent ?? requireConsentFromEnv,
782
+ autoTrackContactLinks: overrides?.autoTrackContactLinks ?? autoTrackContactLinksFromEnv
573
783
  };
574
784
  }
575
785
  var jmpAnalyticsBrowserSingleton = null;