@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.
- package/README.md +11 -0
- package/dist/astro/client.cjs +840 -0
- package/dist/astro/client.cjs.map +1 -0
- package/dist/astro/client.d.cts +28 -0
- package/dist/astro/client.d.ts +28 -0
- package/dist/astro/client.js +69 -0
- package/dist/astro/client.js.map +1 -0
- package/dist/astro/content.cjs +356 -0
- package/dist/astro/content.cjs.map +1 -0
- package/dist/astro/content.d.cts +33 -0
- package/dist/astro/content.d.ts +33 -0
- package/dist/astro/content.js +30 -0
- package/dist/astro/content.js.map +1 -0
- package/dist/astro/integration.cjs +193 -0
- package/dist/astro/integration.cjs.map +1 -0
- package/dist/astro/integration.d.cts +36 -0
- package/dist/astro/integration.d.ts +36 -0
- package/dist/astro/integration.js +36 -0
- package/dist/astro/integration.js.map +1 -0
- package/dist/astro/middleware.cjs +180 -0
- package/dist/astro/middleware.cjs.map +1 -0
- package/dist/astro/middleware.d.cts +19 -0
- package/dist/astro/middleware.d.ts +19 -0
- package/dist/astro/middleware.js +15 -0
- package/dist/astro/middleware.js.map +1 -0
- package/dist/astro/register.cjs +166 -0
- package/dist/astro/register.cjs.map +1 -0
- package/dist/astro/register.d.cts +7 -0
- package/dist/astro/register.d.ts +7 -0
- package/dist/astro/register.js +9 -0
- package/dist/astro/register.js.map +1 -0
- package/dist/chunk-6TDPOZMJ.js +173 -0
- package/dist/chunk-6TDPOZMJ.js.map +1 -0
- package/dist/{chunk-EPNF76VK.js → chunk-DXHJGKIL.js} +2 -2
- package/dist/chunk-GLEOJIKL.js +211 -0
- package/dist/chunk-GLEOJIKL.js.map +1 -0
- package/dist/{chunk-GB37RVJJ.js → chunk-PDOQ2XBW.js} +82 -242
- package/dist/chunk-PDOQ2XBW.js.map +1 -0
- package/dist/chunk-TK4VRDMO.js +19 -0
- package/dist/chunk-TK4VRDMO.js.map +1 -0
- package/dist/chunk-XA55GBSU.js +21 -0
- package/dist/chunk-XA55GBSU.js.map +1 -0
- package/dist/create-jmp-analytics-BdGOgPxl.d.ts +70 -0
- package/dist/create-jmp-analytics-DxhHOmtR.d.cts +70 -0
- package/dist/index.cjs +199 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -341
- package/dist/index.d.ts +16 -341
- package/dist/index.js +7 -5
- package/dist/landing-capture-C8pyZWuF.d.cts +4 -0
- package/dist/landing-capture-C8pyZWuF.d.ts +4 -0
- package/dist/next/content.cjs +135 -88
- package/dist/next/content.cjs.map +1 -1
- package/dist/next/content.d.cts +15 -35
- package/dist/next/content.d.ts +15 -35
- package/dist/next/content.js +30 -159
- package/dist/next/content.js.map +1 -1
- package/dist/next/instrumentation.cjs +64 -29
- package/dist/next/instrumentation.cjs.map +1 -1
- package/dist/next/instrumentation.js +4 -2
- package/dist/next/instrumentation.js.map +1 -1
- package/dist/next/middleware.cjs +68 -36
- package/dist/next/middleware.cjs.map +1 -1
- package/dist/next/middleware.d.cts +6 -9
- package/dist/next/middleware.d.ts +6 -9
- package/dist/next/middleware.js +3 -11
- package/dist/next/middleware.js.map +1 -1
- package/dist/next.cjs +99 -93
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +3 -1
- package/dist/next.d.ts +3 -1
- package/dist/next.js +3 -2
- package/dist/next.js.map +1 -1
- package/dist/react.cjs +99 -93
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -1
- package/dist/react.d.ts +2 -1
- package/dist/react.js +3 -2
- package/dist/site-config-BRuZWGSW.d.cts +46 -0
- package/dist/site-config-hkGMSeT7.d.ts +46 -0
- package/dist/track-page-view-BLS0YfL-.d.ts +9 -0
- package/dist/track-page-view-DfCyWHqe.d.cts +9 -0
- package/dist/types-BtiK_Acj.d.cts +224 -0
- package/dist/types-BtiK_Acj.d.ts +224 -0
- package/docs/astro-integration.md +131 -0
- package/package.json +31 -5
- package/dist/chunk-GB37RVJJ.js.map +0 -1
- /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/
|
|
316
|
-
var
|
|
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
|
-
|
|
393
|
-
|
|
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
|
|
460
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
999
|
-
const
|
|
1000
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
1055
|
-
|
|
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 ??
|
|
1065
|
-
autoTrackContactLinks: overrides?.autoTrackContactLinks ??
|
|
1046
|
+
requireConsent: overrides?.requireConsent ?? pub.requireConsent,
|
|
1047
|
+
autoTrackContactLinks: overrides?.autoTrackContactLinks ?? pub.autoContactLinks
|
|
1066
1048
|
};
|
|
1067
1049
|
}
|
|
1068
1050
|
function getJmpServerContentConfigFromEnv(overrides) {
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
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,
|