@jmp-technologies/analytics 0.1.9 → 0.1.10

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 CHANGED
@@ -51,16 +51,26 @@ export default function RootLayout({ children }: { children: ReactNode }) {
51
51
 
52
52
  That enables **automatic `page_view`** on first load and on client-side navigations (includes query string). No `jmp-analytics-client.ts`, no `useEffect` + `usePathname` boilerplate.
53
53
 
54
- **Optional:** track phone clicks without custom handlers — use **`JmpTrackedTelLink`** from the same module:
54
+ **Optional:** track phone and email clicks without custom handlers — use **`JmpTrackedTelLink`** and **`JmpTrackedMailtoLink`** (since **0.1.10**) from the same module:
55
55
 
56
56
  ```tsx
57
- import { JmpNextAnalytics, JmpTrackedTelLink } from "@jmp-technologies/analytics/next"
57
+ import {
58
+ JmpNextAnalytics,
59
+ JmpTrackedMailtoLink,
60
+ JmpTrackedTelLink,
61
+ } from "@jmp-technologies/analytics/next"
58
62
 
59
63
  <JmpTrackedTelLink href="tel:+15551234567" className="...">
60
64
  Call us
61
65
  </JmpTrackedTelLink>
66
+
67
+ <JmpTrackedMailtoLink href="mailto:hello@example.com" className="...">
68
+ Email us
69
+ </JmpTrackedMailtoLink>
62
70
  ```
63
71
 
72
+ Or call **`trackEmailClick(path)`** / **`trackCallClick(path)`** from `useJmpAnalytics()`.
73
+
64
74
  **Hooks:** `useJmpAnalytics()` from `@jmp-technologies/analytics/next` or `@jmp-technologies/analytics/react` for forms and custom events.
65
75
 
66
76
  ---
@@ -223,11 +233,15 @@ When present, `category` is an optional section label you can use to group FAQs
223
233
  **Next.js (`@jmp-technologies/analytics/next`)**
224
234
 
225
235
  - `JmpNextAnalytics` / `JmpNextPageViewTracker` — App Router page views
226
- - Re-exports: `JmpTrackedTelLink`, `useJmpAnalytics`
236
+ - Re-exports: `JmpTrackedTelLink`, `JmpTrackedMailtoLink`, `useJmpAnalytics`
227
237
 
228
238
  **React (`@jmp-technologies/analytics/react`)**
229
239
 
230
- - `JmpTrackedTelLink`, `useJmpAnalytics` — Vite, CRA, etc. (no automatic route tracking; call `trackPageView` yourself or use your router)
240
+ - `JmpTrackedTelLink`, `JmpTrackedMailtoLink` (since **0.1.10**), `useJmpAnalytics` — Vite, CRA, etc. (no automatic route tracking; call `trackPageView` yourself or use your router)
241
+
242
+ **Tracking events**
243
+
244
+ - `trackPageView`, `trackFormSubmit`, `trackCallClick`, `trackEmailClick` (since **0.1.10**), `trackLead`
231
245
 
232
246
  **Content**
233
247
 
@@ -259,7 +273,9 @@ Package sets `publishConfig.access` to `public`. Requires `npm login` with publi
259
273
  After publish, on client sites:
260
274
 
261
275
  ```bash
262
- npm install @jmp-technologies/analytics@0.1.8
276
+ npm install @jmp-technologies/analytics@latest
263
277
  ```
264
278
 
279
+ Use **≥ 0.1.8** for `getJmpSiteProfile` / `extras`. Use **≥ 0.1.10** for `email_click` tracking and `JmpTrackedMailtoLink`.
280
+
265
281
  Ensure the **dashboard** is deployed with migrations **020** and **021** and `/api/content/site` is live before relying on `getJmpSiteProfile` in production.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getOrCreateJmpAnalyticsFromBrowserEnv
3
- } from "./chunk-XMVG3YES.js";
3
+ } from "./chunk-SUQPZX3K.js";
4
4
 
5
5
  // src/react.tsx
6
6
  import { useMemo } from "react";
@@ -27,9 +27,29 @@ function JmpTrackedTelLink({
27
27
  }
28
28
  );
29
29
  }
30
+ function JmpTrackedMailtoLink({
31
+ href,
32
+ onClick,
33
+ ...rest
34
+ }) {
35
+ return /* @__PURE__ */ jsx(
36
+ "a",
37
+ {
38
+ href,
39
+ ...rest,
40
+ onClick: (e) => {
41
+ const client = getOrCreateJmpAnalyticsFromBrowserEnv();
42
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
43
+ void client?.trackEmailClick(path);
44
+ onClick?.(e);
45
+ }
46
+ }
47
+ );
48
+ }
30
49
 
31
50
  export {
32
51
  useJmpAnalytics,
33
- JmpTrackedTelLink
52
+ JmpTrackedTelLink,
53
+ JmpTrackedMailtoLink
34
54
  };
35
- //# sourceMappingURL=chunk-JX54VUZ7.js.map
55
+ //# sourceMappingURL=chunk-ADX24K7D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.tsx"],"sourcesContent":["\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n\nexport type JmpTrackedMailtoLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `mailto:` link that records an `email_click` with the current path.\n */\nexport function JmpTrackedMailtoLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedMailtoLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackEmailClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n"],"mappings":";;;;;AAEA,SAAS,eAA8C;AAqBnD;AAjBG,SAAS,kBAAuC;AACrD,SAAO,QAAQ,MAAM,sCAAsC,GAAG,CAAC,CAAC;AAClE;AASO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2B;AACzB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,SAAS,CAAC,MAAM;AACd,cAAM,SAAS,sCAAsC;AACrD,cAAM,OACJ,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAC7D,aAAK,QAAQ,eAAe,IAAI;AAChC,kBAAU,CAAC;AAAA,MACb;AAAA;AAAA,EACF;AAEJ;AASO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA8B;AAC5B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,SAAS,CAAC,MAAM;AACd,cAAM,SAAS,sCAAsC;AACrD,cAAM,OACJ,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAC7D,aAAK,QAAQ,gBAAgB,IAAI;AACjC,kBAAU,CAAC;AAAA,MACb;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -204,6 +204,13 @@ function createJmpAnalytics(config) {
204
204
  metadata: metadata ?? {}
205
205
  });
206
206
  }
207
+ function trackEmailClick(path, metadata) {
208
+ return send({
209
+ type: "email_click",
210
+ path: path || "/",
211
+ metadata: metadata ?? {}
212
+ });
213
+ }
207
214
  function trackLead(options) {
208
215
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
209
216
  const body = { path: path || "/" };
@@ -233,7 +240,10 @@ function createJmpAnalytics(config) {
233
240
  if (type === "form_submit") {
234
241
  return trackFormSubmit(payload.path, payload.metadata);
235
242
  }
236
- return trackCallClick(payload.path, payload.metadata);
243
+ if (type === "call_click") {
244
+ return trackCallClick(payload.path, payload.metadata);
245
+ }
246
+ return trackEmailClick(payload.path, payload.metadata);
237
247
  }
238
248
  function subscribeToSpaNavigation() {
239
249
  if (typeof window === "undefined") {
@@ -272,6 +282,7 @@ function createJmpAnalytics(config) {
272
282
  trackPageView,
273
283
  trackFormSubmit,
274
284
  trackCallClick,
285
+ trackEmailClick,
275
286
  trackLead,
276
287
  subscribeToSpaNavigation
277
288
  };
@@ -472,4 +483,4 @@ export {
472
483
  getOrCreateJmpAnalyticsFromBrowserEnv,
473
484
  createJmpContentClientFromServerEnv
474
485
  };
475
- //# sourceMappingURL=chunk-XMVG3YES.js.map
486
+ //# sourceMappingURL=chunk-SUQPZX3K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type JmpEventType =\n | \"page_view\"\n | \"form_submit\"\n | \"call_click\"\n | \"email_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackEmailClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"email_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n if (type === \"call_click\") {\n return trackCallClick(payload.path, payload.metadata)\n }\n return trackEmailClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackEmailClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -242,6 +242,13 @@ function createJmpAnalytics(config) {
242
242
  metadata: metadata ?? {}
243
243
  });
244
244
  }
245
+ function trackEmailClick(path, metadata) {
246
+ return send({
247
+ type: "email_click",
248
+ path: path || "/",
249
+ metadata: metadata ?? {}
250
+ });
251
+ }
245
252
  function trackLead(options) {
246
253
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
247
254
  const body = { path: path || "/" };
@@ -271,7 +278,10 @@ function createJmpAnalytics(config) {
271
278
  if (type === "form_submit") {
272
279
  return trackFormSubmit(payload.path, payload.metadata);
273
280
  }
274
- return trackCallClick(payload.path, payload.metadata);
281
+ if (type === "call_click") {
282
+ return trackCallClick(payload.path, payload.metadata);
283
+ }
284
+ return trackEmailClick(payload.path, payload.metadata);
275
285
  }
276
286
  function subscribeToSpaNavigation() {
277
287
  if (typeof window === "undefined") {
@@ -310,6 +320,7 @@ function createJmpAnalytics(config) {
310
320
  trackPageView,
311
321
  trackFormSubmit,
312
322
  trackCallClick,
323
+ trackEmailClick,
313
324
  trackLead,
314
325
  subscribeToSpaNavigation
315
326
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type JmpEventType = \"page_view\" | \"form_submit\" | \"call_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type JmpEventType =\n | \"page_view\"\n | \"form_submit\"\n | \"call_click\"\n | \"email_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackEmailClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"email_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n if (type === \"call_click\") {\n return trackCallClick(payload.path, payload.metadata)\n }\n return trackEmailClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackEmailClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+JA,IAAM,iBAAiB;AACvB,IAAM,sBAAsB;AAG5B,SAAS,sBAAsB,MAAc;AAC3C,QAAM,IAAI,KAAK,UAAU;AACzB,SACE,EAAE,WAAW,IAAI,KACjB,EAAE,YAAY,EAAE,WAAW,OAAO,KACjC,EAAE,WAAW,GAAG,KAAK,cAAc,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;AAE5D;AAEA,SAAS,iBAAiB,SAAiB;AACzC,SAAO,QAAQ,QAAQ,OAAO,EAAE;AAClC;AAEA,SAAS,UACP,SACA,KACe;AACf,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AACA,MAAI,OAAQ,QAAiC,QAAQ,YAAY;AAC/D,WAAQ,QAAiC,IAAI,GAAG;AAAA,EAClD;AACA,QAAM,SAAS;AACf,QAAM,SAAS,OAAO,GAAG,KAAK,OAAO,IAAI,YAAY,CAAC;AACtD,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO,CAAC,KAAK;AAAA,EACtB;AACA,SAAO,UAAU;AACnB;AAEA,SAAS,mBAAmB,KAAiC;AAC3D,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,WAAO,GAAG,EAAE,QAAQ,GAAG,EAAE,MAAM;AAAA,EACjC,QAAQ;AACN,WAAO,IAAI,WAAW,GAAG,IAAI,MAAM;AAAA,EACrC;AACF;AAEA,SAAS,cAAsB;AAC7B,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AACA,SAAO,OAAO,aAAa,MAAM,WAAW;AAC9C;AAGA,SAAS,uBAA2C;AAClD,MAAI,OAAO,WAAW,eAAe,CAAC,WAAW,QAAQ,YAAY;AACnE,WAAO;AAAA,EACT;AACA,MAAI;AACF,QAAI,KAAK,eAAe,QAAQ,mBAAmB;AACnD,QAAI,CAAC,IAAI;AACP,WAAK,OAAO,WAAW;AACvB,qBAAe,QAAQ,qBAAqB,EAAE;AAAA,IAChD;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,mBAAmB,QAA4B;AAC7D,QAAM,UAAU,iBAAiB,OAAO,OAAO;AAC/C,QAAM,gBACJ,OAAO,cACN,OAAO,eAAe,eAAe,WAAW,QAC7C,WAAW,MAAM,KAAK,UAAU,IAChC;AAEN,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAwB;AAE9B,MAAI,iBAAiB;AACrB,MAAI,eAAe;AAEnB,iBAAe,KAAK,MAA+B;AACjD,UAAM,MAAM,GAAG,OAAO;AACtB,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,sBAAsB,OAAO;AAAA,IAC/B;AAEA,UAAM,MAAM,qBAAqB;AACjC,UAAM,UAAU,EAAE,GAAG,MAAM,GAAI,MAAM,EAAE,WAAW,IAAI,IAAI,CAAC,EAAG;AAE9D,QAAI,OAAO,OAAO;AAChB,cAAQ,MAAM,iCAAiC,QAAQ,KAAK,OAAO;AAAA,IACrE;AAEA,UAAM,MAAM,MAAM,QAAQ,KAAK;AAAA,MAC7B,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,WAAW;AAAA,IACb,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAI,sBAAsB,IAAI,GAAG;AAC/B,cAAM,IAAI;AAAA,UACR,sBAAsB,IAAI,MAAM,WAAM,GAAG;AAAA,QAG3C;AAAA,MACF;AACA,UAAI,SAKA,CAAC;AACL,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS,EAAE,OAAO,KAAK;AAAA,MACzB;AACA,YAAM,SAAS,OAAO,SAAS;AAC/B,YAAM,WAAW;AAAA,QACf,sBAAsB,IAAI,MAAM;AAAA,QAChC,OAAO,OAAO,IAAI,OAAO,IAAI,MAAM;AAAA,QACnC;AAAA,QACA,OAAO,OAAO,SAAS,OAAO,IAAI,KAAK;AAAA,QACvC,OAAO,iBACH,wBAAwB,OAAO,cAAc,KAC7C;AAAA,MACN,EAAE,OAAO,OAAO;AAChB,YAAM,UAAU,SAAS,KAAK,UAAK;AACnC,UAAI,OAAO,OAAO;AAChB,gBAAQ,MAAM,iCAAiC,kBAAkB;AAAA,UAC/D,QAAQ,IAAI;AAAA,UACZ,GAAG;AAAA,UACH,KAAK;AAAA,QACP,CAAC;AAAA,MACH;AACA,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAAA,EACF;AAEA,iBAAe,SAAS,MAA+B;AACrD,UAAM,MAAM,GAAG,OAAO;AACtB,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,sBAAsB,OAAO;AAAA,IAC/B;AAEA,UAAM,MAAM,qBAAqB;AACjC,UAAM,UAAU,EAAE,GAAG,MAAM,GAAI,MAAM,EAAE,WAAW,IAAI,IAAI,CAAC,EAAG;AAE9D,QAAI,OAAO,OAAO;AAChB,cAAQ,MAAM,iCAAiC,QAAQ,KAAK,OAAO;AAAA,IACrE;AAEA,UAAM,MAAM,MAAM,QAAQ,KAAK;AAAA,MAC7B,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,WAAW;AAAA,IACb,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAI,sBAAsB,IAAI,GAAG;AAC/B,cAAM,IAAI;AAAA,UACR,sBAAsB,IAAI,MAAM,WAAM,GAAG;AAAA,QAE3C;AAAA,MACF;AACA,UAAI,SAKA,CAAC;AACL,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS,EAAE,OAAO,KAAK;AAAA,MACzB;AACA,YAAM,SAAS,OAAO,SAAS;AAC/B,YAAM,WAAW;AAAA,QACf,sBAAsB,IAAI,MAAM;AAAA,QAChC,OAAO,OAAO,IAAI,OAAO,IAAI,MAAM;AAAA,QACnC;AAAA,QACA,OAAO,OAAO,SAAS,OAAO,IAAI,KAAK;AAAA,QACvC,OAAO,iBACH,wBAAwB,OAAO,cAAc,KAC7C;AAAA,MACN,EAAE,OAAO,OAAO;AAChB,YAAM,UAAU,SAAS,KAAK,UAAK;AACnC,UAAI,OAAO,OAAO;AAChB,gBAAQ,MAAM,iCAAiC,kBAAkB;AAAA,UAC/D,QAAQ,IAAI;AAAA,UACZ,GAAG;AAAA,UACH,KAAK;AAAA,QACP,CAAC;AAAA,MACH;AACA,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAAA,EACF;AAEA,WAAS,qBAAqB,MAAuB;AACnD,UAAM,KAAK,OAAO,oBAAoB;AACtC,QAAI,MAAM,KAAK,OAAO,WAAW,aAAa;AAC5C,aAAO;AAAA,IACT;AACA,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,SAAS,kBAAkB,MAAM,eAAe,IAAI;AACtD,aAAO;AAAA,IACT;AACA,qBAAiB;AACjB,mBAAe;AACf,WAAO;AAAA,EACT;AAEA,WAAS,cAAc,SAAgC;AACrD,UAAM,OACJ,SAAS,SACR,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAE9D,QAAI,qBAAqB,IAAI,GAAG;AAC9B,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,MAAM,QAAQ;AAAA,MACd,QAAQ,SAAS,UAAU;AAAA,MAC3B,QAAQ,SAAS,UAAU,YAAY;AAAA,MACvC,SAAS,SAAS;AAAA,MAClB,UACE,SAAS,YAAY,OAAO,QAAQ,aAAa,WAC7C,QAAQ,WACR,CAAC;AAAA,IACT,CAAC;AAAA,EACH;AAEA,WAAS,gBACP,MACA,UACA;AACA,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,MAAM,QAAQ;AAAA,MACd,UAAU,YAAY,CAAC;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,WAAS,eACP,MACA,UACA;AACA,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,MAAM,QAAQ;AAAA,MACd,UAAU,YAAY,CAAC;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,WAAS,gBACP,MACA,UACA;AACA,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,MAAM,QAAQ;AAAA,MACd,UAAU,YAAY,CAAC;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,WAAS,UAAU,SAA4B;AAC7C,UAAM,OACJ,SAAS,SACR,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAE9D,UAAM,OAAgC,EAAE,MAAM,QAAQ,IAAI;AAC1D,QAAI,SAAS,KAAM,MAAK,OAAO,QAAQ;AACvC,QAAI,SAAS,MAAO,MAAK,QAAQ,QAAQ;AACzC,QAAI,SAAS,MAAO,MAAK,QAAQ,QAAQ;AACzC,QAAI,SAAS,QAAS,MAAK,UAAU,QAAQ;AAC7C,QAAI,SAAS,QAAS,MAAK,UAAU,QAAQ;AAC7C,QAAI,SAAS,QAAS,MAAK,UAAU,QAAQ;AAC7C,QAAI,SAAS,QAAS,MAAK,UAAU,QAAQ;AAC7C,QAAI,SAAS,YAAY,OAAO,QAAQ,aAAa,UAAU;AAC7D,WAAK,WAAW,QAAQ;AAAA,IAC1B;AACA,QAAI,SAAS,UAAW,MAAK,YAAY,QAAQ;AAEjD,WAAO,SAAS,IAAI;AAAA,EACtB;AAEA,WAAS,MACP,MACA,SAOA;AACA,QAAI,SAAS,aAAa;AACxB,aAAO,cAAc;AAAA,QACnB,MAAM,QAAQ;AAAA,QACd,QAAQ,QAAQ;AAAA,QAChB,QAAQ,QAAQ;AAAA,QAChB,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ;AAAA,MACpB,CAAC;AAAA,IACH;AACA,QAAI,SAAS,eAAe;AAC1B,aAAO,gBAAgB,QAAQ,MAAM,QAAQ,QAAQ;AAAA,IACvD;AACA,QAAI,SAAS,cAAc;AACzB,aAAO,eAAe,QAAQ,MAAM,QAAQ,QAAQ;AAAA,IACtD;AACA,WAAO,gBAAgB,QAAQ,MAAM,QAAQ,QAAQ;AAAA,EACvD;AAOA,WAAS,2BAAuC;AAC9C,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,QAAI,WAAW,OAAO,SAAS;AAE/B,UAAM,MAAM,MAAM;AAChB,YAAM,OAAO,OAAO,SAAS;AAC7B,UAAI,SAAS,UAAU;AACrB,mBAAW;AACX,aAAK,cAAc,EAAE,KAAK,CAAC;AAAA,MAC7B;AAAA,IACF;AAEA,WAAO,iBAAiB,YAAY,GAAG;AAEvC,UAAM,OAAO,QAAQ,UAAU,KAAK,OAAO;AAC3C,UAAM,UAAU,QAAQ,aAAa,KAAK,OAAO;AAEjD,YAAQ,YAAY,IAAI,SAA2C;AACjE,YAAM,IAAI,KAAK,GAAG,IAAI;AACtB,qBAAe,GAAG;AAClB,aAAO;AAAA,IACT;AACA,YAAQ,eAAe,IAAI,SAA8C;AACvE,YAAM,IAAI,QAAQ,GAAG,IAAI;AACzB,qBAAe,GAAG;AAClB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM;AACX,aAAO,oBAAoB,YAAY,GAAG;AAC1C,cAAQ,YAAY;AACpB,cAAQ,eAAe;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAIA,eAAsB,uBACpB,QACA,UAAsC,CAAC,GACxB;AACf,QAAM,UAAU,iBAAiB,OAAO,OAAO;AAC/C,QAAM,gBACJ,OAAO,cACN,OAAO,eAAe,eAAe,WAAW,QAC7C,WAAW,MAAM,KAAK,UAAU,IAChC;AAEN,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,wEAAwE;AAAA,EAC1F;AAEA,QAAM,mBACJ,QAAQ,aAAa,UAAU,QAAQ,SAAS,SAAS,YAAY;AACvE,QAAM,iBACJ,QAAQ,WAAW,UAAU,QAAQ,SAAS,SAAS,SAAS;AAClE,QAAM,OAAO,QAAQ,QAAQ,mBAAmB,QAAQ,SAAS,GAAG;AAEpE,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,IAChB,sBAAsB,OAAO;AAAA,EAC/B;AACA,MAAI,kBAAkB;AACpB,YAAQ,0BAA0B,IAAI;AAAA,EACxC;AACA,MAAI,gBAAgB;AAClB,YAAQ,uBAAuB,IAAI;AAAA,EACrC;AAEA,QAAM,UAAmC;AAAA,IACvC,MAAM;AAAA,IACN,MAAM,QAAQ;AAAA,IACd,UACE,QAAQ,YAAY,OAAO,QAAQ,aAAa,WAC5C,QAAQ,WACR,CAAC;AAAA,EACT;AACA,MAAI,QAAQ,WAAW;AACrB,YAAQ,YAAY,QAAQ;AAAA,EAC9B;AAEA,QAAM,MAAM,GAAG,OAAO;AACtB,MAAI,OAAO,OAAO;AAChB,YAAQ,MAAM,iCAAiC,QAAQ,KAAK,OAAO;AAAA,EACrE;AAEA,QAAM,MAAM,MAAM,cAAc,KAAK;AAAA,IACnC,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU,OAAO;AAAA,EAC9B,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,WAAM,IAAI,EAAE;AAAA,EACrE;AACF;AAEA,eAAsB,gBACpB,QACwB;AACxB,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK;AACd;AAEA,eAAsB,eACpB,QACA,MAC6B;AAC7B,MAAI;AACF,UAAM,OAAO,MAAM;AAAA,MACjB;AAAA,MACA,qBAAqB,mBAAmB,IAAI,CAAC;AAAA,IAC/C;AACA,WAAO,KAAK;AAAA,EACd,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,QAAQ,SAAS,UAAU,GAAG;AAChE,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,WACpB,QACuB;AACvB,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK;AACd;AAEA,eAAsB,kBACpB,QACyB;AACzB,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK;AACd;AAEA,eAAsB,gBAAgB,QAA2C;AAC/E,SAAO,eAAe,QAAQ,qBAAqB;AACrD;AAEO,SAAS,uBAAuB,QAA0B;AAC/D,SAAO;AAAA,IACL,cAAc,MAAM,gBAAgB,MAAM;AAAA,IAC1C,aAAa,CAAC,SAAiB,eAAe,QAAQ,IAAI;AAAA,IAC1D,SAAS,MAAM,WAAW,MAAM;AAAA,IAChC,gBAAgB,MAAM,kBAAkB,MAAM;AAAA,IAC9C,cAAc,MAAM,gBAAgB,MAAM;AAAA,EAC5C;AACF;AAEA,eAAe,WACb,QACA,MACY;AACZ,QAAM,OAAO,MAAM,eAAe,QAAQ,IAAI;AAE9C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,UAAM,UAAU,iBAAiB,OAAO,OAAO;AAC/C,UAAM,MAAM,GAAG,OAAO,GAAG,IAAI;AAC7B,UAAM,IAAI,MAAM,+BAA0B,GAAG,yBAAyB;AAAA,EACxE;AACF;AAEA,eAAe,eACb,QACA,MACiB;AACjB,QAAM,UAAU,iBAAiB,OAAO,OAAO;AAC/C,QAAM,gBACJ,OAAO,cACN,OAAO,eAAe,eAAe,WAAW,QAC7C,WAAW,MAAM,KAAK,UAAU,IAChC;AAEN,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,+DAA+D;AAAA,EACjF;AAEA,QAAM,MAAM,GAAG,OAAO,GAAG,IAAI;AAE7B,MAAI,OAAO,OAAO;AAChB,YAAQ,MAAM,iCAAiC,OAAO,GAAG;AAAA,EAC3D;AAEA,QAAM,MAAM,MAAM,cAAc,KAAK;AAAA,IACnC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,sBAAsB,OAAO;AAAA,IAC/B;AAAA,EACF,CAAC;AAED,QAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,sBAAsB,IAAI,GAAG;AAC/B,YAAM,IAAI;AAAA,QACR,oBAAoB,IAAI,MAAM,WAAM,GAAG;AAAA,MAEzC;AAAA,IACF;AAEA,QAAI,SAA4C,CAAC;AACjD,QAAI;AACF,eAAS,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AACN,eAAS,EAAE,OAAO,KAAK;AAAA,IACzB;AAEA,UAAM,IAAI;AAAA,MACR,CAAC,oBAAoB,IAAI,MAAM,IAAI,OAAO,OAAO,OAAO,IAAI,EACzD,OAAO,OAAO,EACd,KAAK,UAAK;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AACT;AAGO,IAAM,0CAA0C;AAEvD,SAAS,eAAe,KAAiC;AACvD,MAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,IAAK,QAAO;AAC3D,QAAM,IAAI,QAAQ,IAAI,GAAG;AACzB,SAAO,OAAO,MAAM,YAAY,EAAE,SAAS,IAAI,IAAI;AACrD;AAMO,SAAS,oCACd,WAC2B;AAC3B,QAAM,UAAU,eAAe,+BAA+B;AAC9D,QAAM,cAAc,eAAe,8BAA8B;AACjE,MAAI,CAAC,WAAW,CAAC,YAAa,QAAO;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,IAClB,GAAG;AAAA,EACL;AACF;AAQO,SAAS,iCACd,WACyB;AACzB,QAAM,UAAU,eAAe,+BAA+B;AAC9D,QAAM,cAAc,eAAe,8BAA8B;AACjE,MAAI,CAAC,WAAW,CAAC,YAAa,QAAO;AACrC,SAAO,EAAE,SAAS,aAAa,GAAG,UAAU;AAC9C;AAEO,SAAS,qCAA8C;AAC5D,SAAO;AAAA,IACL,eAAe,+BAA+B,KAC5C,eAAe,8BAA8B;AAAA,EACjD;AACF;AAEO,SAAS,kCAA2C;AACzD,SAAO,mCAAmC;AAC5C;AAEA,IAAI,+BAAoD;AAMjD,SAAS,sCACd,WACqB;AACrB,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI,6BAA8B,QAAO;AACzC,QAAM,SAAS,oCAAoC,SAAS;AAC5D,MAAI,CAAC,OAAQ,QAAO;AACpB,iCAA+B,mBAAmB,MAAM;AACxD,SAAO;AACT;AAOO,SAAS,oCACd,WACyB;AACzB,QAAM,SAAS,iCAAiC,SAAS;AACzD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,uBAAuB,MAAM;AACtC;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- type JmpEventType = "page_view" | "form_submit" | "call_click";
1
+ type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click";
2
2
  type JmpAnalyticsConfig = {
3
3
  /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */
4
4
  baseUrl: string;
@@ -145,6 +145,7 @@ declare function createJmpAnalytics(config: JmpAnalyticsConfig): {
145
145
  trackPageView: (options?: TrackPageViewOptions) => Promise<void>;
146
146
  trackFormSubmit: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
147
147
  trackCallClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
148
+ trackEmailClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
148
149
  trackLead: (options?: TrackLeadOptions) => Promise<void>;
149
150
  subscribeToSpaNavigation: () => () => void;
150
151
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type JmpEventType = "page_view" | "form_submit" | "call_click";
1
+ type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click";
2
2
  type JmpAnalyticsConfig = {
3
3
  /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */
4
4
  baseUrl: string;
@@ -145,6 +145,7 @@ declare function createJmpAnalytics(config: JmpAnalyticsConfig): {
145
145
  trackPageView: (options?: TrackPageViewOptions) => Promise<void>;
146
146
  trackFormSubmit: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
147
147
  trackCallClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
148
+ trackEmailClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
148
149
  trackLead: (options?: TrackLeadOptions) => Promise<void>;
149
150
  subscribeToSpaNavigation: () => () => void;
150
151
  };
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  isJmpBrowserAnalyticsEnvConfigured,
15
15
  isJmpServerContentEnvConfigured,
16
16
  trackJmpServerPageView
17
- } from "./chunk-XMVG3YES.js";
17
+ } from "./chunk-SUQPZX3K.js";
18
18
  export {
19
19
  DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
20
20
  createJmpAnalytics,
package/dist/next.cjs CHANGED
@@ -23,6 +23,7 @@ var next_exports = {};
23
23
  __export(next_exports, {
24
24
  JmpNextAnalytics: () => JmpNextAnalytics,
25
25
  JmpNextPageViewTracker: () => JmpNextPageViewTracker,
26
+ JmpTrackedMailtoLink: () => JmpTrackedMailtoLink,
26
27
  JmpTrackedTelLink: () => JmpTrackedTelLink,
27
28
  useJmpAnalytics: () => useJmpAnalytics
28
29
  });
@@ -211,6 +212,13 @@ function createJmpAnalytics(config) {
211
212
  metadata: metadata ?? {}
212
213
  });
213
214
  }
215
+ function trackEmailClick(path, metadata) {
216
+ return send({
217
+ type: "email_click",
218
+ path: path || "/",
219
+ metadata: metadata ?? {}
220
+ });
221
+ }
214
222
  function trackLead(options) {
215
223
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
216
224
  const body = { path: path || "/" };
@@ -240,7 +248,10 @@ function createJmpAnalytics(config) {
240
248
  if (type === "form_submit") {
241
249
  return trackFormSubmit(payload.path, payload.metadata);
242
250
  }
243
- return trackCallClick(payload.path, payload.metadata);
251
+ if (type === "call_click") {
252
+ return trackCallClick(payload.path, payload.metadata);
253
+ }
254
+ return trackEmailClick(payload.path, payload.metadata);
244
255
  }
245
256
  function subscribeToSpaNavigation() {
246
257
  if (typeof window === "undefined") {
@@ -279,6 +290,7 @@ function createJmpAnalytics(config) {
279
290
  trackPageView,
280
291
  trackFormSubmit,
281
292
  trackCallClick,
293
+ trackEmailClick,
282
294
  trackLead,
283
295
  subscribeToSpaNavigation
284
296
  };
@@ -335,6 +347,25 @@ function JmpTrackedTelLink({
335
347
  }
336
348
  );
337
349
  }
350
+ function JmpTrackedMailtoLink({
351
+ href,
352
+ onClick,
353
+ ...rest
354
+ }) {
355
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
356
+ "a",
357
+ {
358
+ href,
359
+ ...rest,
360
+ onClick: (e) => {
361
+ const client = getOrCreateJmpAnalyticsFromBrowserEnv();
362
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
363
+ void client?.trackEmailClick(path);
364
+ onClick?.(e);
365
+ }
366
+ }
367
+ );
368
+ }
338
369
 
339
370
  // src/next.tsx
340
371
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -359,6 +390,7 @@ function JmpNextAnalytics() {
359
390
  0 && (module.exports = {
360
391
  JmpNextAnalytics,
361
392
  JmpNextPageViewTracker,
393
+ JmpTrackedMailtoLink,
362
394
  JmpTrackedTelLink,
363
395
  useJmpAnalytics
364
396
  });
package/dist/next.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.tsx","../src/index.ts","../src/react.tsx"],"sourcesContent":["\"use client\"\n\nimport { Suspense, useEffect } from \"react\"\nimport { usePathname, useSearchParams } from \"next/navigation\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\nimport { JmpTrackedTelLink, useJmpAnalytics } from \"./react\"\n\nexport { JmpTrackedTelLink, useJmpAnalytics }\n\nfunction JmpNextPageViewTrackerInner() {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const query = searchParams?.toString() ?? \"\"\n const path = query ? `${pathname}?${query}` : pathname\n\n useEffect(() => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n void client?.trackPageView({ path })\n }, [path])\n\n return null\n}\n\n/**\n * App Router: tracks `page_view` on first paint and on client navigations.\n * Wraps an inner hook in `Suspense` because `useSearchParams()` requires it.\n */\nexport function JmpNextPageViewTracker() {\n return (\n <Suspense fallback={null}>\n <JmpNextPageViewTrackerInner />\n </Suspense>\n )\n}\n\n/** Same as `JmpNextPageViewTracker` — minimal name for root layouts. */\nexport function JmpNextAnalytics() {\n return <JmpNextPageViewTracker />\n}\n","export type JmpEventType = \"page_view\" | \"form_submit\" | \"call_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n","\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/next.tsx","../src/index.ts","../src/react.tsx"],"sourcesContent":["\"use client\"\n\nimport { Suspense, useEffect } from \"react\"\nimport { usePathname, useSearchParams } from \"next/navigation\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\nimport {\n JmpTrackedMailtoLink,\n JmpTrackedTelLink,\n useJmpAnalytics,\n} from \"./react\"\n\nexport { JmpTrackedMailtoLink, JmpTrackedTelLink, useJmpAnalytics }\n\nfunction JmpNextPageViewTrackerInner() {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const query = searchParams?.toString() ?? \"\"\n const path = query ? `${pathname}?${query}` : pathname\n\n useEffect(() => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n void client?.trackPageView({ path })\n }, [path])\n\n return null\n}\n\n/**\n * App Router: tracks `page_view` on first paint and on client navigations.\n * Wraps an inner hook in `Suspense` because `useSearchParams()` requires it.\n */\nexport function JmpNextPageViewTracker() {\n return (\n <Suspense fallback={null}>\n <JmpNextPageViewTrackerInner />\n </Suspense>\n )\n}\n\n/** Same as `JmpNextPageViewTracker` — minimal name for root layouts. */\nexport function JmpNextAnalytics() {\n return <JmpNextPageViewTracker />\n}\n","export type JmpEventType =\n | \"page_view\"\n | \"form_submit\"\n | \"call_click\"\n | \"email_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackEmailClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"email_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n if (type === \"call_click\") {\n return trackCallClick(payload.path, payload.metadata)\n }\n return trackEmailClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackEmailClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n","\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n\nexport type JmpTrackedMailtoLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `mailto:` link that records an `email_click` with the current path.\n */\nexport function JmpTrackedMailtoLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedMailtoLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackEmailClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
package/dist/next.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- export { JmpTrackedTelLink, useJmpAnalytics } from './react.cjs';
2
+ export { JmpTrackedMailtoLink, JmpTrackedTelLink, useJmpAnalytics } from './react.cjs';
3
3
  import 'react';
4
4
  import './index.cjs';
5
5
 
package/dist/next.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- export { JmpTrackedTelLink, useJmpAnalytics } from './react.js';
2
+ export { JmpTrackedMailtoLink, JmpTrackedTelLink, useJmpAnalytics } from './react.js';
3
3
  import 'react';
4
4
  import './index.js';
5
5
 
package/dist/next.js CHANGED
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import {
3
+ JmpTrackedMailtoLink,
3
4
  JmpTrackedTelLink,
4
5
  useJmpAnalytics
5
- } from "./chunk-JX54VUZ7.js";
6
+ } from "./chunk-ADX24K7D.js";
6
7
  import {
7
8
  getOrCreateJmpAnalyticsFromBrowserEnv
8
- } from "./chunk-XMVG3YES.js";
9
+ } from "./chunk-SUQPZX3K.js";
9
10
 
10
11
  // src/next.tsx
11
12
  import { Suspense, useEffect } from "react";
@@ -31,6 +32,7 @@ function JmpNextAnalytics() {
31
32
  export {
32
33
  JmpNextAnalytics,
33
34
  JmpNextPageViewTracker,
35
+ JmpTrackedMailtoLink,
34
36
  JmpTrackedTelLink,
35
37
  useJmpAnalytics
36
38
  };
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.tsx"],"sourcesContent":["\"use client\"\n\nimport { Suspense, useEffect } from \"react\"\nimport { usePathname, useSearchParams } from \"next/navigation\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\nimport { JmpTrackedTelLink, useJmpAnalytics } from \"./react\"\n\nexport { JmpTrackedTelLink, useJmpAnalytics }\n\nfunction JmpNextPageViewTrackerInner() {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const query = searchParams?.toString() ?? \"\"\n const path = query ? `${pathname}?${query}` : pathname\n\n useEffect(() => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n void client?.trackPageView({ path })\n }, [path])\n\n return null\n}\n\n/**\n * App Router: tracks `page_view` on first paint and on client navigations.\n * Wraps an inner hook in `Suspense` because `useSearchParams()` requires it.\n */\nexport function JmpNextPageViewTracker() {\n return (\n <Suspense fallback={null}>\n <JmpNextPageViewTrackerInner />\n </Suspense>\n )\n}\n\n/** Same as `JmpNextPageViewTracker` — minimal name for root layouts. */\nexport function JmpNextAnalytics() {\n return <JmpNextPageViewTracker />\n}\n"],"mappings":";;;;;;;;;;AAEA,SAAS,UAAU,iBAAiB;AACpC,SAAS,aAAa,uBAAuB;AA2BvC;AArBN,SAAS,8BAA8B;AACrC,QAAM,WAAW,YAAY;AAC7B,QAAM,eAAe,gBAAgB;AACrC,QAAM,QAAQ,cAAc,SAAS,KAAK;AAC1C,QAAM,OAAO,QAAQ,GAAG,QAAQ,IAAI,KAAK,KAAK;AAE9C,YAAU,MAAM;AACd,UAAM,SAAS,sCAAsC;AACrD,SAAK,QAAQ,cAAc,EAAE,KAAK,CAAC;AAAA,EACrC,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AACT;AAMO,SAAS,yBAAyB;AACvC,SACE,oBAAC,YAAS,UAAU,MAClB,8BAAC,+BAA4B,GAC/B;AAEJ;AAGO,SAAS,mBAAmB;AACjC,SAAO,oBAAC,0BAAuB;AACjC;","names":[]}
1
+ {"version":3,"sources":["../src/next.tsx"],"sourcesContent":["\"use client\"\n\nimport { Suspense, useEffect } from \"react\"\nimport { usePathname, useSearchParams } from \"next/navigation\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\nimport {\n JmpTrackedMailtoLink,\n JmpTrackedTelLink,\n useJmpAnalytics,\n} from \"./react\"\n\nexport { JmpTrackedMailtoLink, JmpTrackedTelLink, useJmpAnalytics }\n\nfunction JmpNextPageViewTrackerInner() {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const query = searchParams?.toString() ?? \"\"\n const path = query ? `${pathname}?${query}` : pathname\n\n useEffect(() => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n void client?.trackPageView({ path })\n }, [path])\n\n return null\n}\n\n/**\n * App Router: tracks `page_view` on first paint and on client navigations.\n * Wraps an inner hook in `Suspense` because `useSearchParams()` requires it.\n */\nexport function JmpNextPageViewTracker() {\n return (\n <Suspense fallback={null}>\n <JmpNextPageViewTrackerInner />\n </Suspense>\n )\n}\n\n/** Same as `JmpNextPageViewTracker` — minimal name for root layouts. */\nexport function JmpNextAnalytics() {\n return <JmpNextPageViewTracker />\n}\n"],"mappings":";;;;;;;;;;;AAEA,SAAS,UAAU,iBAAiB;AACpC,SAAS,aAAa,uBAAuB;AA+BvC;AArBN,SAAS,8BAA8B;AACrC,QAAM,WAAW,YAAY;AAC7B,QAAM,eAAe,gBAAgB;AACrC,QAAM,QAAQ,cAAc,SAAS,KAAK;AAC1C,QAAM,OAAO,QAAQ,GAAG,QAAQ,IAAI,KAAK,KAAK;AAE9C,YAAU,MAAM;AACd,UAAM,SAAS,sCAAsC;AACrD,SAAK,QAAQ,cAAc,EAAE,KAAK,CAAC;AAAA,EACrC,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AACT;AAMO,SAAS,yBAAyB;AACvC,SACE,oBAAC,YAAS,UAAU,MAClB,8BAAC,+BAA4B,GAC/B;AAEJ;AAGO,SAAS,mBAAmB;AACjC,SAAO,oBAAC,0BAAuB;AACjC;","names":[]}
package/dist/react.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/react.tsx
22
22
  var react_exports = {};
23
23
  __export(react_exports, {
24
+ JmpTrackedMailtoLink: () => JmpTrackedMailtoLink,
24
25
  JmpTrackedTelLink: () => JmpTrackedTelLink,
25
26
  useJmpAnalytics: () => useJmpAnalytics
26
27
  });
@@ -208,6 +209,13 @@ function createJmpAnalytics(config) {
208
209
  metadata: metadata ?? {}
209
210
  });
210
211
  }
212
+ function trackEmailClick(path, metadata) {
213
+ return send({
214
+ type: "email_click",
215
+ path: path || "/",
216
+ metadata: metadata ?? {}
217
+ });
218
+ }
211
219
  function trackLead(options) {
212
220
  const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
213
221
  const body = { path: path || "/" };
@@ -237,7 +245,10 @@ function createJmpAnalytics(config) {
237
245
  if (type === "form_submit") {
238
246
  return trackFormSubmit(payload.path, payload.metadata);
239
247
  }
240
- return trackCallClick(payload.path, payload.metadata);
248
+ if (type === "call_click") {
249
+ return trackCallClick(payload.path, payload.metadata);
250
+ }
251
+ return trackEmailClick(payload.path, payload.metadata);
241
252
  }
242
253
  function subscribeToSpaNavigation() {
243
254
  if (typeof window === "undefined") {
@@ -276,6 +287,7 @@ function createJmpAnalytics(config) {
276
287
  trackPageView,
277
288
  trackFormSubmit,
278
289
  trackCallClick,
290
+ trackEmailClick,
279
291
  trackLead,
280
292
  subscribeToSpaNavigation
281
293
  };
@@ -331,8 +343,28 @@ function JmpTrackedTelLink({
331
343
  }
332
344
  );
333
345
  }
346
+ function JmpTrackedMailtoLink({
347
+ href,
348
+ onClick,
349
+ ...rest
350
+ }) {
351
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
352
+ "a",
353
+ {
354
+ href,
355
+ ...rest,
356
+ onClick: (e) => {
357
+ const client = getOrCreateJmpAnalyticsFromBrowserEnv();
358
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
359
+ void client?.trackEmailClick(path);
360
+ onClick?.(e);
361
+ }
362
+ }
363
+ );
364
+ }
334
365
  // Annotate the CommonJS export names for ESM import in node:
335
366
  0 && (module.exports = {
367
+ JmpTrackedMailtoLink,
336
368
  JmpTrackedTelLink,
337
369
  useJmpAnalytics
338
370
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.tsx","../src/index.ts"],"sourcesContent":["\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n","export type JmpEventType = \"page_view\" | \"form_submit\" | \"call_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react.tsx","../src/index.ts"],"sourcesContent":["\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n\nexport type JmpTrackedMailtoLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `mailto:` link that records an `email_click` with the current path.\n */\nexport function JmpTrackedMailtoLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedMailtoLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackEmailClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n","export type JmpEventType =\n | \"page_view\"\n | \"form_submit\"\n | \"call_click\"\n | \"email_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackEmailClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"email_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n if (type === \"call_click\") {\n return trackCallClick(payload.path, payload.metadata)\n }\n return trackEmailClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackEmailClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}
package/dist/react.d.cts CHANGED
@@ -10,5 +10,12 @@ type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<"a"> & {
10
10
  * `tel:` (or other) link that records a `call_click` with the current path.
11
11
  */
12
12
  declare function JmpTrackedTelLink({ href, onClick, ...rest }: JmpTrackedTelLinkProps): react_jsx_runtime.JSX.Element;
13
+ type JmpTrackedMailtoLinkProps = ComponentPropsWithoutRef<"a"> & {
14
+ href: string;
15
+ };
16
+ /**
17
+ * `mailto:` link that records an `email_click` with the current path.
18
+ */
19
+ declare function JmpTrackedMailtoLink({ href, onClick, ...rest }: JmpTrackedMailtoLinkProps): react_jsx_runtime.JSX.Element;
13
20
 
14
- export { JmpTrackedTelLink, type JmpTrackedTelLinkProps, useJmpAnalytics };
21
+ export { JmpTrackedMailtoLink, type JmpTrackedMailtoLinkProps, JmpTrackedTelLink, type JmpTrackedTelLinkProps, useJmpAnalytics };
package/dist/react.d.ts CHANGED
@@ -10,5 +10,12 @@ type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<"a"> & {
10
10
  * `tel:` (or other) link that records a `call_click` with the current path.
11
11
  */
12
12
  declare function JmpTrackedTelLink({ href, onClick, ...rest }: JmpTrackedTelLinkProps): react_jsx_runtime.JSX.Element;
13
+ type JmpTrackedMailtoLinkProps = ComponentPropsWithoutRef<"a"> & {
14
+ href: string;
15
+ };
16
+ /**
17
+ * `mailto:` link that records an `email_click` with the current path.
18
+ */
19
+ declare function JmpTrackedMailtoLink({ href, onClick, ...rest }: JmpTrackedMailtoLinkProps): react_jsx_runtime.JSX.Element;
13
20
 
14
- export { JmpTrackedTelLink, type JmpTrackedTelLinkProps, useJmpAnalytics };
21
+ export { JmpTrackedMailtoLink, type JmpTrackedMailtoLinkProps, JmpTrackedTelLink, type JmpTrackedTelLinkProps, useJmpAnalytics };
package/dist/react.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import {
3
+ JmpTrackedMailtoLink,
3
4
  JmpTrackedTelLink,
4
5
  useJmpAnalytics
5
- } from "./chunk-JX54VUZ7.js";
6
- import "./chunk-XMVG3YES.js";
6
+ } from "./chunk-ADX24K7D.js";
7
+ import "./chunk-SUQPZX3K.js";
7
8
  export {
9
+ JmpTrackedMailtoLink,
8
10
  JmpTrackedTelLink,
9
11
  useJmpAnalytics
10
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmp-technologies/analytics",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "JMP Analytics tracking plus blog, FAQ, and site contact profile content for client websites",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [
@@ -74,6 +74,6 @@
74
74
  "typescript": "^5.9.3"
75
75
  },
76
76
  "dependencies": {
77
- "@jmp-technologies/analytics": "^0.1.8"
77
+ "@jmp-technologies/analytics": "^0.1.10"
78
78
  }
79
79
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/react.tsx"],"sourcesContent":["\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n"],"mappings":";;;;;AAEA,SAAS,eAA8C;AAqBnD;AAjBG,SAAS,kBAAuC;AACrD,SAAO,QAAQ,MAAM,sCAAsC,GAAG,CAAC,CAAC;AAClE;AASO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2B;AACzB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,SAAS,CAAC,MAAM;AACd,cAAM,SAAS,sCAAsC;AACrD,cAAM,OACJ,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAC7D,aAAK,QAAQ,eAAe,IAAI;AAChC,kBAAU,CAAC;AAAA,MACb;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type JmpEventType = \"page_view\" | \"form_submit\" | \"call_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}