@jmp-technologies/analytics 0.1.20 → 0.1.21

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 (79) hide show
  1. package/dist/astro/client.cjs +93 -108
  2. package/dist/astro/client.cjs.map +1 -1
  3. package/dist/astro/client.d.cts +2 -1
  4. package/dist/astro/client.d.ts +2 -1
  5. package/dist/astro/client.js +4 -2
  6. package/dist/astro/client.js.map +1 -1
  7. package/dist/astro/content.cjs +96 -92
  8. package/dist/astro/content.cjs.map +1 -1
  9. package/dist/astro/content.d.cts +2 -2
  10. package/dist/astro/content.d.ts +2 -2
  11. package/dist/astro/content.js +3 -2
  12. package/dist/astro/integration.cjs +64 -60
  13. package/dist/astro/integration.cjs.map +1 -1
  14. package/dist/astro/integration.js +3 -2
  15. package/dist/astro/integration.js.map +1 -1
  16. package/dist/astro/middleware.cjs +62 -63
  17. package/dist/astro/middleware.cjs.map +1 -1
  18. package/dist/astro/middleware.d.cts +2 -2
  19. package/dist/astro/middleware.d.ts +2 -2
  20. package/dist/astro/middleware.js +2 -2
  21. package/dist/astro/register.cjs +64 -60
  22. package/dist/astro/register.cjs.map +1 -1
  23. package/dist/astro/register.js +3 -2
  24. package/dist/{chunk-QIWGUF7V.js → chunk-6TDPOZMJ.js} +6 -4
  25. package/dist/{chunk-QIWGUF7V.js.map → chunk-6TDPOZMJ.js.map} +1 -1
  26. package/dist/{chunk-ALTSW5DB.js → chunk-DXHJGKIL.js} +2 -2
  27. package/dist/chunk-GLEOJIKL.js +211 -0
  28. package/dist/chunk-GLEOJIKL.js.map +1 -0
  29. package/dist/{chunk-GFCBZN6S.js → chunk-PDOQ2XBW.js} +71 -271
  30. package/dist/chunk-PDOQ2XBW.js.map +1 -0
  31. package/dist/{chunk-NZS4M3V3.js → chunk-TK4VRDMO.js} +2 -2
  32. package/dist/chunk-TK4VRDMO.js.map +1 -0
  33. package/dist/{chunk-UDNKWFBM.js → chunk-XA55GBSU.js} +5 -3
  34. package/dist/{chunk-UDNKWFBM.js.map → chunk-XA55GBSU.js.map} +1 -1
  35. package/dist/create-jmp-analytics-BdGOgPxl.d.ts +70 -0
  36. package/dist/create-jmp-analytics-DxhHOmtR.d.cts +70 -0
  37. package/dist/index.cjs +189 -194
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +15 -340
  40. package/dist/index.d.ts +15 -340
  41. package/dist/index.js +7 -5
  42. package/dist/landing-capture-C8pyZWuF.d.cts +4 -0
  43. package/dist/landing-capture-C8pyZWuF.d.ts +4 -0
  44. package/dist/next/content.cjs +96 -92
  45. package/dist/next/content.cjs.map +1 -1
  46. package/dist/next/content.d.cts +2 -1
  47. package/dist/next/content.d.ts +2 -1
  48. package/dist/next/content.js +3 -2
  49. package/dist/next/content.js.map +1 -1
  50. package/dist/next/instrumentation.cjs +64 -60
  51. package/dist/next/instrumentation.cjs.map +1 -1
  52. package/dist/next/instrumentation.js +4 -2
  53. package/dist/next/instrumentation.js.map +1 -1
  54. package/dist/next/middleware.cjs +62 -63
  55. package/dist/next/middleware.cjs.map +1 -1
  56. package/dist/next/middleware.d.cts +2 -2
  57. package/dist/next/middleware.d.ts +2 -2
  58. package/dist/next/middleware.js +2 -2
  59. package/dist/next.cjs +96 -109
  60. package/dist/next.cjs.map +1 -1
  61. package/dist/next.d.cts +3 -1
  62. package/dist/next.d.ts +3 -1
  63. package/dist/next.js +3 -2
  64. package/dist/next.js.map +1 -1
  65. package/dist/react.cjs +96 -109
  66. package/dist/react.cjs.map +1 -1
  67. package/dist/react.d.cts +2 -1
  68. package/dist/react.d.ts +2 -1
  69. package/dist/react.js +3 -2
  70. package/dist/site-config-BRuZWGSW.d.cts +46 -0
  71. package/dist/site-config-hkGMSeT7.d.ts +46 -0
  72. package/dist/{track-page-view-CoYo96n0.d.cts → track-page-view-BLS0YfL-.d.ts} +1 -1
  73. package/dist/{track-page-view-DAy0I0Vb.d.ts → track-page-view-DfCyWHqe.d.cts} +1 -1
  74. package/dist/types-BtiK_Acj.d.cts +224 -0
  75. package/dist/types-BtiK_Acj.d.ts +224 -0
  76. package/package.json +2 -4
  77. package/dist/chunk-GFCBZN6S.js.map +0 -1
  78. package/dist/chunk-NZS4M3V3.js.map +0 -1
  79. /package/dist/{chunk-ALTSW5DB.js.map → chunk-DXHJGKIL.js.map} +0 -0
@@ -1,7 +1,9 @@
1
1
  import {
2
- getJmpContentConfig,
3
2
  registerJmpContentWrappers
4
- } from "./chunk-GFCBZN6S.js";
3
+ } from "./chunk-PDOQ2XBW.js";
4
+ import {
5
+ getJmpContentConfig
6
+ } from "./chunk-GLEOJIKL.js";
5
7
 
6
8
  // src/astro/register.ts
7
9
  async function registerJmpAstroContent() {
@@ -16,4 +18,4 @@ async function registerJmpAstroContent() {
16
18
  export {
17
19
  registerJmpAstroContent
18
20
  };
19
- //# sourceMappingURL=chunk-UDNKWFBM.js.map
21
+ //# sourceMappingURL=chunk-XA55GBSU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/astro/register.ts"],"sourcesContent":["import { registerJmpContentWrappers } from \"../index\"\nimport { getJmpContentConfig } from \"../site-config\"\n\n/**\n * Register content wrappers with the dashboard (call once per build).\n * Import `./jmp.config` in `astro.config.mjs` before calling this, or use {@link jmpAnalytics} integration.\n */\nexport async function registerJmpAstroContent(): Promise<void> {\n const config = getJmpContentConfig()\n if (!config) {\n return\n }\n await registerJmpContentWrappers(config).catch(() => {})\n}\n"],"mappings":";;;;;;AAOA,eAAsB,0BAAyC;AAC7D,QAAM,SAAS,oBAAoB;AACnC,MAAI,CAAC,QAAQ;AACX;AAAA,EACF;AACA,QAAM,2BAA2B,MAAM,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACzD;","names":[]}
1
+ {"version":3,"sources":["../src/astro/register.ts"],"sourcesContent":["import { registerJmpContentWrappers } from \"../index\"\nimport { getJmpContentConfig } from \"../site-config\"\n\n/**\n * Register content wrappers with the dashboard (call once per build).\n * Import `./jmp.config` in `astro.config.mjs` before calling this, or use {@link jmpAnalytics} integration.\n */\nexport async function registerJmpAstroContent(): Promise<void> {\n const config = getJmpContentConfig()\n if (!config) {\n return\n }\n await registerJmpContentWrappers(config).catch(() => {})\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,0BAAyC;AAC7D,QAAM,SAAS,oBAAoB;AACnC,MAAI,CAAC,QAAQ;AACX;AAAA,EACF;AACA,QAAM,2BAA2B,MAAM,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACzD;","names":[]}
@@ -0,0 +1,70 @@
1
+ import { h as JmpAnalyticsConfig, i as JmpEventType, T as TrackPageViewOptions, j as TrackLeadOptions } from './types-BtiK_Acj.js';
2
+
3
+ /**
4
+ * Classify outbound contact links for auto-tracking.
5
+ *
6
+ * Keep channel set in sync with `lib/ingest/contact-click.ts` on the dashboard side.
7
+ *
8
+ * Channels detected from `href` alone (no widget / iframe detection):
9
+ *
10
+ * - `call` — `tel:` (delegates to existing `trackCallClick`)
11
+ * - `email` — `mailto:` (delegates to existing `trackEmailClick`)
12
+ * - `sms` — `sms:` / `smsto:`
13
+ * - `whatsapp` — `wa.me`, `api.whatsapp.com`, `whatsapp.com/send`
14
+ * - `maps` — Google Maps, Apple Maps, `maps.app.goo.gl`, `geo:`
15
+ * - `booking` — fixed allowlist of common scheduling hosts (see {@link BOOKING_LINK_HOSTS})
16
+ *
17
+ * `linkHost` returned in `contact_click` results is the hostname only — never the full URL
18
+ * (avoids leaking phone numbers in `tel:`/`sms:` URLs and PII-laden booking query strings).
19
+ */
20
+ type ContactLinkChannel = "sms" | "whatsapp" | "maps" | "booking";
21
+ type ClassifiedContactLink = {
22
+ kind: "call";
23
+ } | {
24
+ kind: "email";
25
+ } | {
26
+ kind: "contact_click";
27
+ channel: ContactLinkChannel;
28
+ linkHost: string | null;
29
+ };
30
+ /** v1 booking hosts (~80%+ SMB coverage). Add hosts here when a client uses something rare. */
31
+ declare const BOOKING_LINK_HOSTS: readonly ["calendly.com", "cal.com", "acuityscheduling.com", "calendar.google.com", "calendar.app.google", "bookings.microsoft.com", "outlook.office.com", "outlook.office365.com", "book.ms", "setmore.com", "simplybook.me", "oncehub.com", "scheduleonce.com", "youcanbook.me", "savvycal.com", "tidycal.com", "zohobookings.com", "square.site", "squareup.com", "meetings.hubspot.com", "jobber.com", "housecallpro.com", "servicetitan.com", "thryv.com", "honeybook.com", "dubsado.com", "fresha.com", "booksy.com", "vagaro.com", "mindbodyonline.com", "mindbody.io", "janeapp.com", "cliniko.com", "practicebetter.io"];
32
+ /** Returns `null` when the link should not be tracked as a contact link. */
33
+ declare function classifyContactHref(href: string): ClassifiedContactLink | null;
34
+
35
+ /** Whether analytics events may be sent (after CMP / cookie banner). */
36
+ declare function isJmpTrackingConsentGranted(): boolean;
37
+ /** Subscribe to consent changes (for React `useSyncExternalStore`). */
38
+ declare function subscribeJmpTrackingConsent(listener: () => void): () => void;
39
+ /**
40
+ * Grant or deny analytics. Persists in `localStorage` when `requireConsent` is used.
41
+ * Call from your cookie banner before tracking runs.
42
+ */
43
+ declare function setJmpTrackingConsent(granted: boolean): void;
44
+
45
+ declare function createJmpAnalytics(config: JmpAnalyticsConfig): {
46
+ track: (type: JmpEventType, payload: {
47
+ path: string;
48
+ metadata?: Record<string, unknown>;
49
+ source?: string;
50
+ device?: string;
51
+ country?: string;
52
+ }) => Promise<void>;
53
+ trackPageView: (options?: TrackPageViewOptions) => Promise<void>;
54
+ trackFormSubmit: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
55
+ trackCallClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
56
+ trackEmailClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
57
+ trackContactClick: (path: string, options: {
58
+ channel: ContactLinkChannel;
59
+ linkHost?: string | null;
60
+ metadata?: Record<string, unknown>;
61
+ }) => Promise<void>;
62
+ trackLead: (options?: TrackLeadOptions) => Promise<void>;
63
+ subscribeToSpaNavigation: () => () => void;
64
+ subscribeContactLinkAutoTrack: () => () => void;
65
+ setTrackingConsent: typeof setJmpTrackingConsent;
66
+ isTrackingConsentGranted: typeof isJmpTrackingConsentGranted;
67
+ };
68
+ type JmpAnalytics = ReturnType<typeof createJmpAnalytics>;
69
+
70
+ export { BOOKING_LINK_HOSTS as B, type ClassifiedContactLink as C, type JmpAnalytics as J, subscribeJmpTrackingConsent as a, type ContactLinkChannel as b, classifyContactHref as c, createJmpAnalytics as d, isJmpTrackingConsentGranted as i, setJmpTrackingConsent as s };
@@ -0,0 +1,70 @@
1
+ import { h as JmpAnalyticsConfig, i as JmpEventType, T as TrackPageViewOptions, j as TrackLeadOptions } from './types-BtiK_Acj.cjs';
2
+
3
+ /**
4
+ * Classify outbound contact links for auto-tracking.
5
+ *
6
+ * Keep channel set in sync with `lib/ingest/contact-click.ts` on the dashboard side.
7
+ *
8
+ * Channels detected from `href` alone (no widget / iframe detection):
9
+ *
10
+ * - `call` — `tel:` (delegates to existing `trackCallClick`)
11
+ * - `email` — `mailto:` (delegates to existing `trackEmailClick`)
12
+ * - `sms` — `sms:` / `smsto:`
13
+ * - `whatsapp` — `wa.me`, `api.whatsapp.com`, `whatsapp.com/send`
14
+ * - `maps` — Google Maps, Apple Maps, `maps.app.goo.gl`, `geo:`
15
+ * - `booking` — fixed allowlist of common scheduling hosts (see {@link BOOKING_LINK_HOSTS})
16
+ *
17
+ * `linkHost` returned in `contact_click` results is the hostname only — never the full URL
18
+ * (avoids leaking phone numbers in `tel:`/`sms:` URLs and PII-laden booking query strings).
19
+ */
20
+ type ContactLinkChannel = "sms" | "whatsapp" | "maps" | "booking";
21
+ type ClassifiedContactLink = {
22
+ kind: "call";
23
+ } | {
24
+ kind: "email";
25
+ } | {
26
+ kind: "contact_click";
27
+ channel: ContactLinkChannel;
28
+ linkHost: string | null;
29
+ };
30
+ /** v1 booking hosts (~80%+ SMB coverage). Add hosts here when a client uses something rare. */
31
+ declare const BOOKING_LINK_HOSTS: readonly ["calendly.com", "cal.com", "acuityscheduling.com", "calendar.google.com", "calendar.app.google", "bookings.microsoft.com", "outlook.office.com", "outlook.office365.com", "book.ms", "setmore.com", "simplybook.me", "oncehub.com", "scheduleonce.com", "youcanbook.me", "savvycal.com", "tidycal.com", "zohobookings.com", "square.site", "squareup.com", "meetings.hubspot.com", "jobber.com", "housecallpro.com", "servicetitan.com", "thryv.com", "honeybook.com", "dubsado.com", "fresha.com", "booksy.com", "vagaro.com", "mindbodyonline.com", "mindbody.io", "janeapp.com", "cliniko.com", "practicebetter.io"];
32
+ /** Returns `null` when the link should not be tracked as a contact link. */
33
+ declare function classifyContactHref(href: string): ClassifiedContactLink | null;
34
+
35
+ /** Whether analytics events may be sent (after CMP / cookie banner). */
36
+ declare function isJmpTrackingConsentGranted(): boolean;
37
+ /** Subscribe to consent changes (for React `useSyncExternalStore`). */
38
+ declare function subscribeJmpTrackingConsent(listener: () => void): () => void;
39
+ /**
40
+ * Grant or deny analytics. Persists in `localStorage` when `requireConsent` is used.
41
+ * Call from your cookie banner before tracking runs.
42
+ */
43
+ declare function setJmpTrackingConsent(granted: boolean): void;
44
+
45
+ declare function createJmpAnalytics(config: JmpAnalyticsConfig): {
46
+ track: (type: JmpEventType, payload: {
47
+ path: string;
48
+ metadata?: Record<string, unknown>;
49
+ source?: string;
50
+ device?: string;
51
+ country?: string;
52
+ }) => Promise<void>;
53
+ trackPageView: (options?: TrackPageViewOptions) => Promise<void>;
54
+ trackFormSubmit: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
55
+ trackCallClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
56
+ trackEmailClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
57
+ trackContactClick: (path: string, options: {
58
+ channel: ContactLinkChannel;
59
+ linkHost?: string | null;
60
+ metadata?: Record<string, unknown>;
61
+ }) => Promise<void>;
62
+ trackLead: (options?: TrackLeadOptions) => Promise<void>;
63
+ subscribeToSpaNavigation: () => () => void;
64
+ subscribeContactLinkAutoTrack: () => () => void;
65
+ setTrackingConsent: typeof setJmpTrackingConsent;
66
+ isTrackingConsentGranted: typeof isJmpTrackingConsentGranted;
67
+ };
68
+ type JmpAnalytics = ReturnType<typeof createJmpAnalytics>;
69
+
70
+ export { BOOKING_LINK_HOSTS as B, type ClassifiedContactLink as C, type JmpAnalytics as J, subscribeJmpTrackingConsent as a, type ContactLinkChannel as b, classifyContactHref as c, createJmpAnalytics as d, isJmpTrackingConsentGranted as i, setJmpTrackingConsent as s };
package/dist/index.cjs CHANGED
@@ -312,72 +312,12 @@ function classifyContactHref(href) {
312
312
  return null;
313
313
  }
314
314
 
315
- // src/env-public.ts
316
- function readProcessEnv(name) {
317
- if (typeof process === "undefined" || !process.env) return void 0;
318
- const v = process.env[name];
319
- return typeof v === "string" && v.length > 0 ? v : void 0;
320
- }
321
- function firstDefined(...values) {
322
- for (const v of values) {
323
- if (v) return v;
324
- }
325
- return void 0;
326
- }
327
- function readJmpPublicEnvFromProcess(overrides) {
328
- const analyticsUrl = firstDefined(
329
- overrides?.analyticsUrl,
330
- readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL"),
331
- readProcessEnv("PUBLIC_JMP_ANALYTICS_URL")
332
- );
333
- const trackingKey = firstDefined(
334
- overrides?.trackingKey,
335
- readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY"),
336
- readProcessEnv("PUBLIC_JMP_TRACKING_KEY")
337
- );
338
- const requireConsentRaw = firstDefined(
339
- overrides?.requireConsent !== void 0 ? String(overrides.requireConsent) : void 0,
340
- readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT"),
341
- readProcessEnv("PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT")
342
- );
343
- const autoContactRaw = firstDefined(
344
- overrides?.autoContactLinks !== void 0 ? String(overrides.autoContactLinks) : void 0,
345
- readProcessEnv("NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS"),
346
- readProcessEnv("PUBLIC_JMP_AUTO_CONTACT_LINKS")
347
- );
348
- return {
349
- analyticsUrl,
350
- trackingKey,
351
- requireConsent: requireConsentRaw === "true",
352
- autoContactLinks: autoContactRaw === "true"
353
- };
354
- }
355
-
356
- // src/site-config.ts
357
- var registeredSite = null;
358
- function defineJmpSite(definition) {
359
- registeredSite = definition;
360
- return definition;
361
- }
362
- function getJmpSiteDefinition() {
363
- return registeredSite;
364
- }
365
- function getJmpContentConfig() {
366
- const { analyticsUrl, trackingKey } = readJmpPublicEnvFromProcess();
367
- if (!analyticsUrl || !trackingKey) {
368
- return null;
369
- }
370
- const site = getJmpSiteDefinition();
371
- return {
372
- baseUrl: analyticsUrl,
373
- trackingKey,
374
- contentWrappers: site?.contentWrappers ?? void 0
375
- };
376
- }
377
-
378
- // src/index.ts
379
- var JMP_ANALYTICS_SDK_VERSION = "0.1.20";
315
+ // src/constants.ts
316
+ var JMP_ANALYTICS_SDK_VERSION = "0.1.21";
380
317
  var JMP_TRACKING_CONSENT_STORAGE_KEY = "jmp_analytics_consent_v1";
318
+ var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
319
+
320
+ // src/consent.ts
381
321
  var moduleTrackingConsentGranted = true;
382
322
  var trackingConsentListeners = /* @__PURE__ */ new Set();
383
323
  function readStoredTrackingConsent() {
@@ -390,6 +330,9 @@ function readStoredTrackingConsent() {
390
330
  return false;
391
331
  }
392
332
  }
333
+ function initJmpTrackingConsent(requireConsent) {
334
+ moduleTrackingConsentGranted = requireConsent ? readStoredTrackingConsent() : true;
335
+ }
393
336
  function isJmpTrackingConsentGranted() {
394
337
  return moduleTrackingConsentGranted;
395
338
  }
@@ -420,33 +363,8 @@ function setJmpTrackingConsent(granted) {
420
363
  listener();
421
364
  }
422
365
  }
423
- function captureAndGetLandingReferer() {
424
- if (typeof window === "undefined") {
425
- return null;
426
- }
427
- return captureLandingAttribution(window.location.hostname).landingReferer;
428
- }
429
- function mergeSessionUtms(metadata) {
430
- if (typeof window === "undefined") {
431
- return;
432
- }
433
- const { utm } = captureLandingAttribution(window.location.hostname);
434
- for (const key of JMP_UTM_PARAM_KEYS) {
435
- if (metadata[key] === void 0 && utm[key]) {
436
- metadata[key] = utm[key];
437
- }
438
- }
439
- }
440
- function splitPathAndSearch(pathInput) {
441
- const qIndex = pathInput.indexOf("?");
442
- if (qIndex === -1) {
443
- return { pathname: pathInput || "/", search: "" };
444
- }
445
- return {
446
- pathname: pathInput.slice(0, qIndex) || "/",
447
- search: pathInput.slice(qIndex + 1)
448
- };
449
- }
366
+
367
+ // src/http.ts
450
368
  function responseLooksLikeHtml(body) {
451
369
  const s = body.trimStart();
452
370
  return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
@@ -479,24 +397,75 @@ function pathFromRequestUrl(url) {
479
397
  return url.startsWith("/") ? url : "/";
480
398
  }
481
399
  }
400
+ function resolveFetch(fetchImpl, context) {
401
+ const resolved = fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
402
+ if (!resolved) {
403
+ throw new Error(`${context}: fetch is not available; pass fetchImpl in config`);
404
+ }
405
+ return resolved;
406
+ }
407
+ function formatApiErrorMessage(status, text, prefix) {
408
+ if (responseLooksLikeHtml(text)) {
409
+ return `${prefix} HTTP ${status} \u2014 response was HTML (not the JSON API). Check baseUrl points at the JMP Analytics dashboard.`;
410
+ }
411
+ let parsed = {};
412
+ try {
413
+ parsed = JSON.parse(text);
414
+ } catch {
415
+ parsed = { error: text };
416
+ }
417
+ const detail = parsed.error ?? text;
418
+ return [
419
+ `${prefix} HTTP ${status}`,
420
+ parsed.code ? `[${parsed.code}]` : null,
421
+ detail,
422
+ parsed.hint ? `Hint: ${parsed.hint}` : null,
423
+ parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
424
+ ].filter(Boolean).join(" \u2014 ");
425
+ }
426
+
427
+ // src/tracker/helpers.ts
428
+ function captureAndGetLandingReferer() {
429
+ if (typeof window === "undefined") {
430
+ return null;
431
+ }
432
+ return captureLandingAttribution(window.location.hostname).landingReferer;
433
+ }
434
+ function mergeSessionUtms(metadata) {
435
+ if (typeof window === "undefined") {
436
+ return;
437
+ }
438
+ const { utm } = captureLandingAttribution(window.location.hostname);
439
+ for (const key of JMP_UTM_PARAM_KEYS) {
440
+ if (metadata[key] === void 0 && utm[key]) {
441
+ metadata[key] = utm[key];
442
+ }
443
+ }
444
+ }
445
+ function splitPathAndSearch(pathInput) {
446
+ const qIndex = pathInput.indexOf("?");
447
+ if (qIndex === -1) {
448
+ return { pathname: pathInput || "/", search: "" };
449
+ }
450
+ return {
451
+ pathname: pathInput.slice(0, qIndex) || "/",
452
+ search: pathInput.slice(qIndex + 1)
453
+ };
454
+ }
482
455
  function inferDevice() {
483
456
  if (typeof window === "undefined") {
484
457
  return "unknown";
485
458
  }
486
459
  return window.innerWidth < 768 ? "mobile" : "desktop";
487
460
  }
461
+
462
+ // src/tracker/create-jmp-analytics.ts
488
463
  function createJmpAnalytics(config) {
489
464
  const baseUrl = normalizeBaseUrl(config.baseUrl);
490
- const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
491
- if (!resolvedFetch) {
492
- throw new Error(
493
- "createJmpAnalytics: fetch is not available; pass fetchImpl in config"
494
- );
495
- }
496
- const fetchFn = resolvedFetch;
497
- moduleTrackingConsentGranted = config.requireConsent ? readStoredTrackingConsent() : true;
465
+ const fetchFn = resolveFetch(config.fetchImpl, "createJmpAnalytics");
466
+ initJmpTrackingConsent(config.requireConsent);
498
467
  function canSend() {
499
- return moduleTrackingConsentGranted;
468
+ return isJmpTrackingConsentGranted();
500
469
  }
501
470
  let lastDedupePath = "";
502
471
  let lastDedupeAt = 0;
@@ -528,32 +497,9 @@ function createJmpAnalytics(config) {
528
497
  });
529
498
  if (!res.ok) {
530
499
  const text = await res.text();
531
- if (responseLooksLikeHtml(text)) {
532
- throw new Error(
533
- `JMP Analytics HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), not a marketing or client site unless that codebase ships the same API route.`
534
- );
535
- }
536
- let parsed = {};
537
- try {
538
- parsed = JSON.parse(text);
539
- } catch {
540
- parsed = { error: text };
541
- }
542
- const detail = parsed.error ?? text;
543
- const segments = [
544
- `JMP Analytics HTTP ${res.status}`,
545
- parsed.code ? `[${parsed.code}]` : null,
546
- detail,
547
- parsed.hint ? `Hint: ${parsed.hint}` : null,
548
- parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
549
- ].filter(Boolean);
550
- const message = segments.join(" \u2014 ");
500
+ const message = formatApiErrorMessage(res.status, text, "JMP Analytics");
551
501
  if (config.debug) {
552
- console.debug("[@jmp-technologies/analytics]", "error response", {
553
- status: res.status,
554
- ...parsed,
555
- raw: text
556
- });
502
+ console.debug("[@jmp-technologies/analytics]", "error response", { status: res.status, raw: text });
557
503
  }
558
504
  throw new Error(message);
559
505
  }
@@ -580,32 +526,9 @@ function createJmpAnalytics(config) {
580
526
  });
581
527
  if (!res.ok) {
582
528
  const text = await res.text();
583
- if (responseLooksLikeHtml(text)) {
584
- throw new Error(
585
- `JMP Analytics HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`
586
- );
587
- }
588
- let parsed = {};
589
- try {
590
- parsed = JSON.parse(text);
591
- } catch {
592
- parsed = { error: text };
593
- }
594
- const detail = parsed.error ?? text;
595
- const segments = [
596
- `JMP Analytics HTTP ${res.status}`,
597
- parsed.code ? `[${parsed.code}]` : null,
598
- detail,
599
- parsed.hint ? `Hint: ${parsed.hint}` : null,
600
- parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
601
- ].filter(Boolean);
602
- const message = segments.join(" \u2014 ");
529
+ const message = formatApiErrorMessage(res.status, text, "JMP Analytics");
603
530
  if (config.debug) {
604
- console.debug("[@jmp-technologies/analytics]", "error response", {
605
- status: res.status,
606
- ...parsed,
607
- raw: text
608
- });
531
+ console.debug("[@jmp-technologies/analytics]", "error response", { status: res.status, raw: text });
609
532
  }
610
533
  throw new Error(message);
611
534
  }
@@ -869,6 +792,8 @@ function createJmpAnalytics(config) {
869
792
  isTrackingConsentGranted: isJmpTrackingConsentGranted
870
793
  };
871
794
  }
795
+
796
+ // src/server/page-view.ts
872
797
  async function trackJmpServerPageView(config, options = {}) {
873
798
  const baseUrl = normalizeBaseUrl(config.baseUrl);
874
799
  const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -910,6 +835,55 @@ async function trackJmpServerPageView(config, options = {}) {
910
835
  throw new Error(`JMP server analytics HTTP ${res.status} \u2014 ${text}`);
911
836
  }
912
837
  }
838
+
839
+ // src/content/fetch.ts
840
+ async function getContent(config, path) {
841
+ const text = await getContentText(config, path);
842
+ try {
843
+ return JSON.parse(text);
844
+ } catch {
845
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
846
+ const url = `${baseUrl}${path}`;
847
+ throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
848
+ }
849
+ }
850
+ async function getContentText(config, path) {
851
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
852
+ const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
853
+ if (!resolvedFetch) {
854
+ throw new Error("JMP content: fetch is not available; pass fetchImpl in config");
855
+ }
856
+ const url = `${baseUrl}${path}`;
857
+ if (config.debug) {
858
+ console.debug("[@jmp-technologies/analytics]", "GET", url);
859
+ }
860
+ const res = await resolvedFetch(url, {
861
+ method: "GET",
862
+ headers: {
863
+ "X-JMP-Tracking-Key": config.trackingKey
864
+ }
865
+ });
866
+ const text = await res.text();
867
+ if (!res.ok) {
868
+ if (responseLooksLikeHtml(text)) {
869
+ throw new Error(
870
+ `JMP content HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl to your JMP Analytics dashboard origin.`
871
+ );
872
+ }
873
+ let parsed = {};
874
+ try {
875
+ parsed = JSON.parse(text);
876
+ } catch {
877
+ parsed = { error: text };
878
+ }
879
+ throw new Error(
880
+ [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
881
+ );
882
+ }
883
+ return text;
884
+ }
885
+
886
+ // src/content/api.ts
913
887
  async function getJmpBlogPosts(config) {
914
888
  const data = await getContent(
915
889
  config,
@@ -1005,6 +979,8 @@ async function registerJmpContentWrappers(config) {
1005
979
  const data = JSON.parse(text);
1006
980
  return { ok: true, wrappers: data.wrappers ?? wrappers };
1007
981
  }
982
+
983
+ // src/content/client.ts
1008
984
  function createJmpContentClient(config) {
1009
985
  return {
1010
986
  getBlogPosts: () => getJmpBlogPosts(config),
@@ -1016,52 +992,49 @@ function createJmpContentClient(config) {
1016
992
  registerContentWrappers: () => registerJmpContentWrappers(config)
1017
993
  };
1018
994
  }
1019
- async function getContent(config, path) {
1020
- const text = await getContentText(config, path);
1021
- try {
1022
- return JSON.parse(text);
1023
- } catch {
1024
- const baseUrl = normalizeBaseUrl(config.baseUrl);
1025
- const url = `${baseUrl}${path}`;
1026
- throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
1027
- }
995
+
996
+ // src/env-public.ts
997
+ function readProcessEnv(name) {
998
+ if (typeof process === "undefined" || !process.env) return void 0;
999
+ const v = process.env[name];
1000
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1028
1001
  }
1029
- async function getContentText(config, path) {
1030
- const baseUrl = normalizeBaseUrl(config.baseUrl);
1031
- const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
1032
- if (!resolvedFetch) {
1033
- throw new Error("JMP content: fetch is not available; pass fetchImpl in config");
1034
- }
1035
- const url = `${baseUrl}${path}`;
1036
- if (config.debug) {
1037
- console.debug("[@jmp-technologies/analytics]", "GET", url);
1038
- }
1039
- const res = await resolvedFetch(url, {
1040
- method: "GET",
1041
- headers: {
1042
- "X-JMP-Tracking-Key": config.trackingKey
1043
- }
1044
- });
1045
- const text = await res.text();
1046
- if (!res.ok) {
1047
- if (responseLooksLikeHtml(text)) {
1048
- throw new Error(
1049
- `JMP content HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl to your JMP Analytics dashboard origin.`
1050
- );
1051
- }
1052
- let parsed = {};
1053
- try {
1054
- parsed = JSON.parse(text);
1055
- } catch {
1056
- parsed = { error: text };
1057
- }
1058
- throw new Error(
1059
- [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
1060
- );
1002
+ function firstDefined(...values) {
1003
+ for (const v of values) {
1004
+ if (v) return v;
1061
1005
  }
1062
- return text;
1006
+ return void 0;
1063
1007
  }
1064
- var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
1008
+ function readJmpPublicEnvFromProcess(overrides) {
1009
+ const analyticsUrl = firstDefined(
1010
+ overrides?.analyticsUrl,
1011
+ readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL"),
1012
+ readProcessEnv("PUBLIC_JMP_ANALYTICS_URL")
1013
+ );
1014
+ const trackingKey = firstDefined(
1015
+ overrides?.trackingKey,
1016
+ readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY"),
1017
+ readProcessEnv("PUBLIC_JMP_TRACKING_KEY")
1018
+ );
1019
+ const requireConsentRaw = firstDefined(
1020
+ overrides?.requireConsent !== void 0 ? String(overrides.requireConsent) : void 0,
1021
+ readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT"),
1022
+ readProcessEnv("PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT")
1023
+ );
1024
+ const autoContactRaw = firstDefined(
1025
+ overrides?.autoContactLinks !== void 0 ? String(overrides.autoContactLinks) : void 0,
1026
+ readProcessEnv("NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS"),
1027
+ readProcessEnv("PUBLIC_JMP_AUTO_CONTACT_LINKS")
1028
+ );
1029
+ return {
1030
+ analyticsUrl,
1031
+ trackingKey,
1032
+ requireConsent: requireConsentRaw === "true",
1033
+ autoContactLinks: autoContactRaw === "true"
1034
+ };
1035
+ }
1036
+
1037
+ // src/env/browser.ts
1065
1038
  function getJmpBrowserAnalyticsConfigFromEnv(overrides) {
1066
1039
  const pub = readJmpPublicEnvFromProcess();
1067
1040
  if (!pub.analyticsUrl || !pub.trackingKey) return null;
@@ -1100,6 +1073,28 @@ function createJmpContentClientFromServerEnv(overrides) {
1100
1073
  if (!config) return null;
1101
1074
  return createJmpContentClient(config);
1102
1075
  }
1076
+
1077
+ // src/site-config.ts
1078
+ var registeredSite = null;
1079
+ function defineJmpSite(definition) {
1080
+ registeredSite = definition;
1081
+ return definition;
1082
+ }
1083
+ function getJmpSiteDefinition() {
1084
+ return registeredSite;
1085
+ }
1086
+ function getJmpContentConfig() {
1087
+ const { analyticsUrl, trackingKey } = readJmpPublicEnvFromProcess();
1088
+ if (!analyticsUrl || !trackingKey) {
1089
+ return null;
1090
+ }
1091
+ const site = getJmpSiteDefinition();
1092
+ return {
1093
+ baseUrl: analyticsUrl,
1094
+ trackingKey,
1095
+ contentWrappers: site?.contentWrappers ?? void 0
1096
+ };
1097
+ }
1103
1098
  // Annotate the CommonJS export names for ESM import in node:
1104
1099
  0 && (module.exports = {
1105
1100
  BOOKING_LINK_HOSTS,