@jmp-technologies/analytics 0.1.16 → 0.1.18

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/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",
@@ -119,11 +118,9 @@ function captureLandingAttribution(siteHostname) {
119
118
  const preservedReferer = storedReferer || cookieReferer;
120
119
  const currentReferer = document.referrer?.trim() || null;
121
120
  if (currentReferer && isExternalReferrerUrl(currentReferer, siteHostname)) {
122
- if (!storedReferer) {
123
- sessionStorage.setItem(JMP_LANDING_REFERER_KEY, currentReferer);
124
- }
121
+ sessionStorage.setItem(JMP_LANDING_REFERER_KEY, currentReferer);
125
122
  writeLandingRefererCookie(currentReferer, siteHostname);
126
- landingReferer = preservedReferer || currentReferer;
123
+ landingReferer = currentReferer;
127
124
  } else if (preservedReferer) {
128
125
  landingReferer = preservedReferer;
129
126
  if (!storedReferer) {
@@ -168,75 +165,164 @@ function getOrCreateSessionIdFromStorage() {
168
165
  return void 0;
169
166
  }
170
167
  }
171
- function wasEarlyBeaconSentForPath(path) {
172
- if (typeof window === "undefined") {
173
- 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;
174
215
  }
175
- try {
176
- return sessionStorage.getItem(JMP_EARLY_BEACON_PATH_KEY) === path;
177
- } catch {
178
- return false;
216
+ if (hostnameMatches(host, "maps.app.goo.gl")) {
217
+ return true;
179
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;
180
223
  }
181
- function markEarlyBeaconSentForPath(path) {
182
- if (typeof window === "undefined") {
183
- 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" };
184
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;
185
266
  try {
186
- sessionStorage.setItem(JMP_EARLY_BEACON_PATH_KEY, path);
267
+ url = new URL(raw);
187
268
  } catch {
269
+ return null;
270
+ }
271
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
272
+ return null;
188
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;
189
286
  }
190
- function sendEarlyPageViewBeacon(args) {
191
- 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") {
192
294
  return false;
193
295
  }
194
- const path = `${window.location.pathname}${window.location.search}` || "/";
195
- if (wasEarlyBeaconSentForPath(path)) {
296
+ try {
297
+ return localStorage.getItem(JMP_TRACKING_CONSENT_STORAGE_KEY) === "granted";
298
+ } catch {
196
299
  return false;
197
300
  }
198
- const { landingReferer, utm } = captureLandingAttribution(window.location.hostname);
199
- const sessionId = getOrCreateSessionIdFromStorage();
200
- const metadata = {
201
- pageHostname: window.location.hostname,
202
- earlyBeacon: true,
203
- ...utm
204
- };
205
- if (landingReferer) {
206
- metadata.referer = landingReferer;
207
- }
208
- const payload = {
209
- type: "page_view",
210
- path,
211
- trackingKey: args.trackingKey,
212
- ...sessionId ? { sessionId } : {},
213
- metadata
214
- };
215
- const headers = {
216
- "Content-Type": "application/json",
217
- "X-JMP-Tracking-Key": args.trackingKey
218
- };
219
- if (landingReferer) {
220
- headers["X-JMP-Visitor-Referer"] = landingReferer;
221
- }
222
- markEarlyBeaconSentForPath(path);
223
- void fetch(`${args.baseUrl.replace(/\/$/, "")}/api/events`, {
224
- method: "POST",
225
- headers,
226
- body: JSON.stringify(payload),
227
- keepalive: true,
228
- mode: "cors",
229
- credentials: "omit"
230
- }).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") {
231
311
  try {
232
- sessionStorage.removeItem(JMP_EARLY_BEACON_PATH_KEY);
312
+ localStorage.setItem(
313
+ JMP_TRACKING_CONSENT_STORAGE_KEY,
314
+ granted ? "granted" : "denied"
315
+ );
233
316
  } catch {
234
317
  }
235
- });
236
- return true;
318
+ if (granted) {
319
+ captureLandingAttribution(window.location.hostname);
320
+ }
321
+ }
322
+ for (const listener of trackingConsentListeners) {
323
+ listener();
324
+ }
237
325
  }
238
-
239
- // src/index.ts
240
326
  function captureAndGetLandingReferer() {
241
327
  if (typeof window === "undefined") {
242
328
  return null;
@@ -286,9 +372,16 @@ function createJmpAnalytics(config) {
286
372
  );
287
373
  }
288
374
  const fetchFn = resolvedFetch;
375
+ moduleTrackingConsentGranted = config.requireConsent ? readStoredTrackingConsent() : true;
376
+ function canSend() {
377
+ return moduleTrackingConsentGranted;
378
+ }
289
379
  let lastDedupePath = "";
290
380
  let lastDedupeAt = 0;
291
381
  async function send(body) {
382
+ if (!canSend()) {
383
+ return;
384
+ }
292
385
  const url = `${baseUrl}/api/events`;
293
386
  const headers = {
294
387
  "Content-Type": "application/json",
@@ -344,6 +437,9 @@ function createJmpAnalytics(config) {
344
437
  }
345
438
  }
346
439
  async function sendLead(body) {
440
+ if (!canSend()) {
441
+ return;
442
+ }
347
443
  const url = `${baseUrl}/api/leads`;
348
444
  const headers = {
349
445
  "Content-Type": "application/json",
@@ -406,6 +502,9 @@ function createJmpAnalytics(config) {
406
502
  return false;
407
503
  }
408
504
  function trackPageView(options) {
505
+ if (!canSend()) {
506
+ return Promise.resolve();
507
+ }
409
508
  const pathFromOptions = options?.path?.trim();
410
509
  let pathname = "/";
411
510
  let search = "";
@@ -421,9 +520,6 @@ function createJmpAnalytics(config) {
421
520
  search = window.location.search.replace(/^\?/, "");
422
521
  }
423
522
  const path = search ? `${pathname}?${search}` : pathname;
424
- if (wasEarlyBeaconSentForPath(path)) {
425
- return Promise.resolve();
426
- }
427
523
  if (shouldDedupePageView(pathname)) {
428
524
  return Promise.resolve();
429
525
  }
@@ -474,6 +570,20 @@ function createJmpAnalytics(config) {
474
570
  metadata: metadata ?? {}
475
571
  });
476
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
+ }
477
587
  function trackLead(options) {
478
588
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
479
589
  const body = { path: path || "/" };
@@ -506,7 +616,20 @@ function createJmpAnalytics(config) {
506
616
  if (type === "call_click") {
507
617
  return trackCallClick(payload.path, payload.metadata);
508
618
  }
509
- 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
+ });
510
633
  }
511
634
  function subscribeToSpaNavigation() {
512
635
  if (typeof window === "undefined") {
@@ -540,12 +663,75 @@ function createJmpAnalytics(config) {
540
663
  history.replaceState = replace;
541
664
  };
542
665
  }
543
- 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()) {
544
731
  captureLandingAttribution(window.location.hostname);
545
- sendEarlyPageViewBeacon({
546
- baseUrl,
547
- trackingKey: config.trackingKey
548
- });
732
+ }
733
+ if (typeof window !== "undefined" && config.autoTrackContactLinks && canSend()) {
734
+ subscribeContactLinkAutoTrack();
549
735
  }
550
736
  return {
551
737
  track,
@@ -553,8 +739,12 @@ function createJmpAnalytics(config) {
553
739
  trackFormSubmit,
554
740
  trackCallClick,
555
741
  trackEmailClick,
742
+ trackContactClick,
556
743
  trackLead,
557
- subscribeToSpaNavigation
744
+ subscribeToSpaNavigation,
745
+ subscribeContactLinkAutoTrack,
746
+ setTrackingConsent: setJmpTrackingConsent,
747
+ isTrackingConsentGranted: isJmpTrackingConsentGranted
558
748
  };
559
749
  }
560
750
  var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
@@ -567,11 +757,15 @@ function getJmpBrowserAnalyticsConfigFromEnv(overrides) {
567
757
  const baseUrl = readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
568
758
  const trackingKey = readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
569
759
  if (!baseUrl || !trackingKey) return null;
760
+ const requireConsentFromEnv = readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT") === "true";
761
+ const autoTrackContactLinksFromEnv = readProcessEnv("NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS") === "true";
570
762
  return {
571
763
  baseUrl,
572
764
  trackingKey,
573
765
  pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
574
- ...overrides
766
+ ...overrides,
767
+ requireConsent: overrides?.requireConsent ?? requireConsentFromEnv,
768
+ autoTrackContactLinks: overrides?.autoTrackContactLinks ?? autoTrackContactLinksFromEnv
575
769
  };
576
770
  }
577
771
  var jmpAnalyticsBrowserSingleton = null;