@jmp-technologies/analytics 0.1.19 → 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 (88) hide show
  1. package/README.md +11 -0
  2. package/dist/astro/client.cjs +840 -0
  3. package/dist/astro/client.cjs.map +1 -0
  4. package/dist/astro/client.d.cts +28 -0
  5. package/dist/astro/client.d.ts +28 -0
  6. package/dist/astro/client.js +69 -0
  7. package/dist/astro/client.js.map +1 -0
  8. package/dist/astro/content.cjs +356 -0
  9. package/dist/astro/content.cjs.map +1 -0
  10. package/dist/astro/content.d.cts +33 -0
  11. package/dist/astro/content.d.ts +33 -0
  12. package/dist/astro/content.js +30 -0
  13. package/dist/astro/content.js.map +1 -0
  14. package/dist/astro/integration.cjs +193 -0
  15. package/dist/astro/integration.cjs.map +1 -0
  16. package/dist/astro/integration.d.cts +36 -0
  17. package/dist/astro/integration.d.ts +36 -0
  18. package/dist/astro/integration.js +36 -0
  19. package/dist/astro/integration.js.map +1 -0
  20. package/dist/astro/middleware.cjs +180 -0
  21. package/dist/astro/middleware.cjs.map +1 -0
  22. package/dist/astro/middleware.d.cts +19 -0
  23. package/dist/astro/middleware.d.ts +19 -0
  24. package/dist/astro/middleware.js +15 -0
  25. package/dist/astro/middleware.js.map +1 -0
  26. package/dist/astro/register.cjs +166 -0
  27. package/dist/astro/register.cjs.map +1 -0
  28. package/dist/astro/register.d.cts +7 -0
  29. package/dist/astro/register.d.ts +7 -0
  30. package/dist/astro/register.js +9 -0
  31. package/dist/astro/register.js.map +1 -0
  32. package/dist/chunk-6TDPOZMJ.js +173 -0
  33. package/dist/chunk-6TDPOZMJ.js.map +1 -0
  34. package/dist/{chunk-EPNF76VK.js → chunk-DXHJGKIL.js} +2 -2
  35. package/dist/chunk-GLEOJIKL.js +211 -0
  36. package/dist/chunk-GLEOJIKL.js.map +1 -0
  37. package/dist/{chunk-GB37RVJJ.js → chunk-PDOQ2XBW.js} +82 -242
  38. package/dist/chunk-PDOQ2XBW.js.map +1 -0
  39. package/dist/chunk-TK4VRDMO.js +19 -0
  40. package/dist/chunk-TK4VRDMO.js.map +1 -0
  41. package/dist/chunk-XA55GBSU.js +21 -0
  42. package/dist/chunk-XA55GBSU.js.map +1 -0
  43. package/dist/create-jmp-analytics-BdGOgPxl.d.ts +70 -0
  44. package/dist/create-jmp-analytics-DxhHOmtR.d.cts +70 -0
  45. package/dist/index.cjs +199 -197
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +16 -341
  48. package/dist/index.d.ts +16 -341
  49. package/dist/index.js +7 -5
  50. package/dist/landing-capture-C8pyZWuF.d.cts +4 -0
  51. package/dist/landing-capture-C8pyZWuF.d.ts +4 -0
  52. package/dist/next/content.cjs +135 -88
  53. package/dist/next/content.cjs.map +1 -1
  54. package/dist/next/content.d.cts +15 -35
  55. package/dist/next/content.d.ts +15 -35
  56. package/dist/next/content.js +30 -159
  57. package/dist/next/content.js.map +1 -1
  58. package/dist/next/instrumentation.cjs +64 -29
  59. package/dist/next/instrumentation.cjs.map +1 -1
  60. package/dist/next/instrumentation.js +4 -2
  61. package/dist/next/instrumentation.js.map +1 -1
  62. package/dist/next/middleware.cjs +68 -36
  63. package/dist/next/middleware.cjs.map +1 -1
  64. package/dist/next/middleware.d.cts +6 -9
  65. package/dist/next/middleware.d.ts +6 -9
  66. package/dist/next/middleware.js +3 -11
  67. package/dist/next/middleware.js.map +1 -1
  68. package/dist/next.cjs +99 -93
  69. package/dist/next.cjs.map +1 -1
  70. package/dist/next.d.cts +3 -1
  71. package/dist/next.d.ts +3 -1
  72. package/dist/next.js +3 -2
  73. package/dist/next.js.map +1 -1
  74. package/dist/react.cjs +99 -93
  75. package/dist/react.cjs.map +1 -1
  76. package/dist/react.d.cts +2 -1
  77. package/dist/react.d.ts +2 -1
  78. package/dist/react.js +3 -2
  79. package/dist/site-config-BRuZWGSW.d.cts +46 -0
  80. package/dist/site-config-hkGMSeT7.d.ts +46 -0
  81. package/dist/track-page-view-BLS0YfL-.d.ts +9 -0
  82. package/dist/track-page-view-DfCyWHqe.d.cts +9 -0
  83. package/dist/types-BtiK_Acj.d.cts +224 -0
  84. package/dist/types-BtiK_Acj.d.ts +224 -0
  85. package/docs/astro-integration.md +131 -0
  86. package/package.json +31 -5
  87. package/dist/chunk-GB37RVJJ.js.map +0 -1
  88. /package/dist/{chunk-EPNF76VK.js.map → chunk-DXHJGKIL.js.map} +0 -0
@@ -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,41 +312,12 @@ function classifyContactHref(href) {
312
312
  return null;
313
313
  }
314
314
 
315
- // src/site-config.ts
316
- var registeredSite = null;
317
- function readNextPublicEnv(key) {
318
- if (typeof process === "undefined" || !process.env) return void 0;
319
- const v = key === "NEXT_PUBLIC_JMP_ANALYTICS_URL" ? process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL : process.env.NEXT_PUBLIC_JMP_TRACKING_KEY;
320
- return typeof v === "string" && v.length > 0 ? v : void 0;
321
- }
322
- function getServerConfigFromEnv() {
323
- const baseUrl = readNextPublicEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
324
- const trackingKey = readNextPublicEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
325
- if (!baseUrl || !trackingKey) return null;
326
- return { baseUrl, trackingKey };
327
- }
328
- function defineJmpSite(definition) {
329
- registeredSite = definition;
330
- return definition;
331
- }
332
- function getJmpSiteDefinition() {
333
- return registeredSite;
334
- }
335
- function getJmpContentConfig() {
336
- const fromEnv = getServerConfigFromEnv();
337
- if (!fromEnv) {
338
- return null;
339
- }
340
- const site = getJmpSiteDefinition();
341
- return {
342
- ...fromEnv,
343
- contentWrappers: site?.contentWrappers ?? fromEnv.contentWrappers
344
- };
345
- }
346
-
347
- // src/index.ts
348
- var JMP_ANALYTICS_SDK_VERSION = "0.1.19";
315
+ // src/constants.ts
316
+ var JMP_ANALYTICS_SDK_VERSION = "0.1.21";
349
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
350
321
  var moduleTrackingConsentGranted = true;
351
322
  var trackingConsentListeners = /* @__PURE__ */ new Set();
352
323
  function readStoredTrackingConsent() {
@@ -359,6 +330,9 @@ function readStoredTrackingConsent() {
359
330
  return false;
360
331
  }
361
332
  }
333
+ function initJmpTrackingConsent(requireConsent) {
334
+ moduleTrackingConsentGranted = requireConsent ? readStoredTrackingConsent() : true;
335
+ }
362
336
  function isJmpTrackingConsentGranted() {
363
337
  return moduleTrackingConsentGranted;
364
338
  }
@@ -389,33 +363,8 @@ function setJmpTrackingConsent(granted) {
389
363
  listener();
390
364
  }
391
365
  }
392
- function captureAndGetLandingReferer() {
393
- if (typeof window === "undefined") {
394
- return null;
395
- }
396
- return captureLandingAttribution(window.location.hostname).landingReferer;
397
- }
398
- function mergeSessionUtms(metadata) {
399
- if (typeof window === "undefined") {
400
- return;
401
- }
402
- const { utm } = captureLandingAttribution(window.location.hostname);
403
- for (const key of JMP_UTM_PARAM_KEYS) {
404
- if (metadata[key] === void 0 && utm[key]) {
405
- metadata[key] = utm[key];
406
- }
407
- }
408
- }
409
- function splitPathAndSearch(pathInput) {
410
- const qIndex = pathInput.indexOf("?");
411
- if (qIndex === -1) {
412
- return { pathname: pathInput || "/", search: "" };
413
- }
414
- return {
415
- pathname: pathInput.slice(0, qIndex) || "/",
416
- search: pathInput.slice(qIndex + 1)
417
- };
418
- }
366
+
367
+ // src/http.ts
419
368
  function responseLooksLikeHtml(body) {
420
369
  const s = body.trimStart();
421
370
  return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
@@ -448,24 +397,75 @@ function pathFromRequestUrl(url) {
448
397
  return url.startsWith("/") ? url : "/";
449
398
  }
450
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
+ }
451
455
  function inferDevice() {
452
456
  if (typeof window === "undefined") {
453
457
  return "unknown";
454
458
  }
455
459
  return window.innerWidth < 768 ? "mobile" : "desktop";
456
460
  }
461
+
462
+ // src/tracker/create-jmp-analytics.ts
457
463
  function createJmpAnalytics(config) {
458
464
  const baseUrl = normalizeBaseUrl(config.baseUrl);
459
- const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
460
- if (!resolvedFetch) {
461
- throw new Error(
462
- "createJmpAnalytics: fetch is not available; pass fetchImpl in config"
463
- );
464
- }
465
- const fetchFn = resolvedFetch;
466
- moduleTrackingConsentGranted = config.requireConsent ? readStoredTrackingConsent() : true;
465
+ const fetchFn = resolveFetch(config.fetchImpl, "createJmpAnalytics");
466
+ initJmpTrackingConsent(config.requireConsent);
467
467
  function canSend() {
468
- return moduleTrackingConsentGranted;
468
+ return isJmpTrackingConsentGranted();
469
469
  }
470
470
  let lastDedupePath = "";
471
471
  let lastDedupeAt = 0;
@@ -497,32 +497,9 @@ function createJmpAnalytics(config) {
497
497
  });
498
498
  if (!res.ok) {
499
499
  const text = await res.text();
500
- if (responseLooksLikeHtml(text)) {
501
- throw new Error(
502
- `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.`
503
- );
504
- }
505
- let parsed = {};
506
- try {
507
- parsed = JSON.parse(text);
508
- } catch {
509
- parsed = { error: text };
510
- }
511
- const detail = parsed.error ?? text;
512
- const segments = [
513
- `JMP Analytics HTTP ${res.status}`,
514
- parsed.code ? `[${parsed.code}]` : null,
515
- detail,
516
- parsed.hint ? `Hint: ${parsed.hint}` : null,
517
- parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
518
- ].filter(Boolean);
519
- const message = segments.join(" \u2014 ");
500
+ const message = formatApiErrorMessage(res.status, text, "JMP Analytics");
520
501
  if (config.debug) {
521
- console.debug("[@jmp-technologies/analytics]", "error response", {
522
- status: res.status,
523
- ...parsed,
524
- raw: text
525
- });
502
+ console.debug("[@jmp-technologies/analytics]", "error response", { status: res.status, raw: text });
526
503
  }
527
504
  throw new Error(message);
528
505
  }
@@ -549,32 +526,9 @@ function createJmpAnalytics(config) {
549
526
  });
550
527
  if (!res.ok) {
551
528
  const text = await res.text();
552
- if (responseLooksLikeHtml(text)) {
553
- throw new Error(
554
- `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).`
555
- );
556
- }
557
- let parsed = {};
558
- try {
559
- parsed = JSON.parse(text);
560
- } catch {
561
- parsed = { error: text };
562
- }
563
- const detail = parsed.error ?? text;
564
- const segments = [
565
- `JMP Analytics HTTP ${res.status}`,
566
- parsed.code ? `[${parsed.code}]` : null,
567
- detail,
568
- parsed.hint ? `Hint: ${parsed.hint}` : null,
569
- parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
570
- ].filter(Boolean);
571
- const message = segments.join(" \u2014 ");
529
+ const message = formatApiErrorMessage(res.status, text, "JMP Analytics");
572
530
  if (config.debug) {
573
- console.debug("[@jmp-technologies/analytics]", "error response", {
574
- status: res.status,
575
- ...parsed,
576
- raw: text
577
- });
531
+ console.debug("[@jmp-technologies/analytics]", "error response", { status: res.status, raw: text });
578
532
  }
579
533
  throw new Error(message);
580
534
  }
@@ -838,6 +792,8 @@ function createJmpAnalytics(config) {
838
792
  isTrackingConsentGranted: isJmpTrackingConsentGranted
839
793
  };
840
794
  }
795
+
796
+ // src/server/page-view.ts
841
797
  async function trackJmpServerPageView(config, options = {}) {
842
798
  const baseUrl = normalizeBaseUrl(config.baseUrl);
843
799
  const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -879,6 +835,55 @@ async function trackJmpServerPageView(config, options = {}) {
879
835
  throw new Error(`JMP server analytics HTTP ${res.status} \u2014 ${text}`);
880
836
  }
881
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
882
887
  async function getJmpBlogPosts(config) {
883
888
  const data = await getContent(
884
889
  config,
@@ -974,6 +979,8 @@ async function registerJmpContentWrappers(config) {
974
979
  const data = JSON.parse(text);
975
980
  return { ok: true, wrappers: data.wrappers ?? wrappers };
976
981
  }
982
+
983
+ // src/content/client.ts
977
984
  function createJmpContentClient(config) {
978
985
  return {
979
986
  getBlogPosts: () => getJmpBlogPosts(config),
@@ -985,96 +992,69 @@ function createJmpContentClient(config) {
985
992
  registerContentWrappers: () => registerJmpContentWrappers(config)
986
993
  };
987
994
  }
988
- async function getContent(config, path) {
989
- const text = await getContentText(config, path);
990
- try {
991
- return JSON.parse(text);
992
- } catch {
993
- const baseUrl = normalizeBaseUrl(config.baseUrl);
994
- const url = `${baseUrl}${path}`;
995
- throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
996
- }
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;
997
1001
  }
998
- async function getContentText(config, path) {
999
- const baseUrl = normalizeBaseUrl(config.baseUrl);
1000
- const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
1001
- if (!resolvedFetch) {
1002
- throw new Error("JMP content: fetch is not available; pass fetchImpl in config");
1002
+ function firstDefined(...values) {
1003
+ for (const v of values) {
1004
+ if (v) return v;
1003
1005
  }
1004
- const url = `${baseUrl}${path}`;
1005
- if (config.debug) {
1006
- console.debug("[@jmp-technologies/analytics]", "GET", url);
1007
- }
1008
- const res = await resolvedFetch(url, {
1009
- method: "GET",
1010
- headers: {
1011
- "X-JMP-Tracking-Key": config.trackingKey
1012
- }
1013
- });
1014
- const text = await res.text();
1015
- if (!res.ok) {
1016
- if (responseLooksLikeHtml(text)) {
1017
- throw new Error(
1018
- `JMP content HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl to your JMP Analytics dashboard origin.`
1019
- );
1020
- }
1021
- let parsed = {};
1022
- try {
1023
- parsed = JSON.parse(text);
1024
- } catch {
1025
- parsed = { error: text };
1026
- }
1027
- throw new Error(
1028
- [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
1029
- );
1030
- }
1031
- return text;
1006
+ return void 0;
1032
1007
  }
1033
- var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
1034
- function readNextPublicEnv2(key) {
1035
- if (typeof process === "undefined" || !process.env) return void 0;
1036
- let v;
1037
- switch (key) {
1038
- case "NEXT_PUBLIC_JMP_ANALYTICS_URL":
1039
- v = process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL;
1040
- break;
1041
- case "NEXT_PUBLIC_JMP_TRACKING_KEY":
1042
- v = process.env.NEXT_PUBLIC_JMP_TRACKING_KEY;
1043
- break;
1044
- case "NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT":
1045
- v = process.env.NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT;
1046
- break;
1047
- case "NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS":
1048
- v = process.env.NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS;
1049
- break;
1050
- }
1051
- return typeof v === "string" && v.length > 0 ? v : void 0;
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
+ };
1052
1035
  }
1036
+
1037
+ // src/env/browser.ts
1053
1038
  function getJmpBrowserAnalyticsConfigFromEnv(overrides) {
1054
- const baseUrl = readNextPublicEnv2("NEXT_PUBLIC_JMP_ANALYTICS_URL");
1055
- const trackingKey = readNextPublicEnv2("NEXT_PUBLIC_JMP_TRACKING_KEY");
1056
- if (!baseUrl || !trackingKey) return null;
1057
- const requireConsentFromEnv = readNextPublicEnv2("NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT") === "true";
1058
- const autoTrackContactLinksFromEnv = readNextPublicEnv2("NEXT_PUBLIC_JMP_AUTO_CONTACT_LINKS") === "true";
1039
+ const pub = readJmpPublicEnvFromProcess();
1040
+ if (!pub.analyticsUrl || !pub.trackingKey) return null;
1059
1041
  return {
1060
- baseUrl,
1061
- trackingKey,
1042
+ baseUrl: pub.analyticsUrl,
1043
+ trackingKey: pub.trackingKey,
1062
1044
  pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
1063
1045
  ...overrides,
1064
- requireConsent: overrides?.requireConsent ?? requireConsentFromEnv,
1065
- autoTrackContactLinks: overrides?.autoTrackContactLinks ?? autoTrackContactLinksFromEnv
1046
+ requireConsent: overrides?.requireConsent ?? pub.requireConsent,
1047
+ autoTrackContactLinks: overrides?.autoTrackContactLinks ?? pub.autoContactLinks
1066
1048
  };
1067
1049
  }
1068
1050
  function getJmpServerContentConfigFromEnv(overrides) {
1069
- const baseUrl = readNextPublicEnv2("NEXT_PUBLIC_JMP_ANALYTICS_URL");
1070
- const trackingKey = readNextPublicEnv2("NEXT_PUBLIC_JMP_TRACKING_KEY");
1071
- if (!baseUrl || !trackingKey) return null;
1072
- return { baseUrl, trackingKey, ...overrides };
1051
+ const pub = readJmpPublicEnvFromProcess();
1052
+ if (!pub.analyticsUrl || !pub.trackingKey) return null;
1053
+ return { baseUrl: pub.analyticsUrl, trackingKey: pub.trackingKey, ...overrides };
1073
1054
  }
1074
1055
  function isJmpBrowserAnalyticsEnvConfigured() {
1075
- return Boolean(
1076
- readNextPublicEnv2("NEXT_PUBLIC_JMP_ANALYTICS_URL") && readNextPublicEnv2("NEXT_PUBLIC_JMP_TRACKING_KEY")
1077
- );
1056
+ const pub = readJmpPublicEnvFromProcess();
1057
+ return Boolean(pub.analyticsUrl && pub.trackingKey);
1078
1058
  }
1079
1059
  function isJmpServerContentEnvConfigured() {
1080
1060
  return isJmpBrowserAnalyticsEnvConfigured();
@@ -1093,6 +1073,28 @@ function createJmpContentClientFromServerEnv(overrides) {
1093
1073
  if (!config) return null;
1094
1074
  return createJmpContentClient(config);
1095
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
+ }
1096
1098
  // Annotate the CommonJS export names for ESM import in node:
1097
1099
  0 && (module.exports = {
1098
1100
  BOOKING_LINK_HOSTS,