@jmp-technologies/analytics 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K47RWY4L.js → chunk-ST5BERIA.js} +175 -12
- package/dist/chunk-ST5BERIA.js.map +1 -0
- package/dist/{chunk-PTNZJRSO.js → chunk-ZLWI2UAP.js} +2 -2
- package/dist/index.cjs +176 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.ts +57 -2
- package/dist/index.js +5 -1
- package/dist/next.cjs +100 -10
- package/dist/next.cjs.map +1 -1
- package/dist/next.js +2 -2
- package/dist/react.cjs +100 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-K47RWY4L.js.map +0 -1
- /package/dist/{chunk-PTNZJRSO.js.map → chunk-ZLWI2UAP.js.map} +0 -0
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 {\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 JmpTrustSummary = {\n google_rating: number | null\n google_review_count: number | null\n google_reviews_url: string | null\n yelp_url: string | null\n trustpilot_url: string | null\n disclaimer: string | null\n}\n\nexport type JmpTestimonial = {\n id: string\n slug: string\n body: string\n author_name: string\n author_descriptor: string | null\n location: string | null\n rating: number | null\n image_url: string | null\n video_url: string | null\n category: string | null\n source: string | null\n featured: boolean\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustStat = {\n id: string\n slug: string\n label: string\n value: string\n sort_order: number\n updated_at: string\n}\n\nexport type JmpPartnerLogo = {\n id: string\n slug: string\n name: string\n image_url: string | null\n link_url: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpCredential = {\n id: string\n slug: string\n title: string\n issuer: string | null\n year: string | null\n image_url: string | null\n category: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustBundle = {\n summary: JmpTrustSummary\n testimonials: JmpTestimonial[]\n stats: JmpTrustStat[]\n logos: JmpPartnerLogo[]\n credentials: JmpCredential[]\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 pathname =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n const search =\n typeof window !== \"undefined\" ? window.location.search.replace(/^\\?/, \"\") : \"\"\n const path = search ? `${pathname}?${search}` : pathname\n\n if (shouldDedupePageView(pathname)) {\n return Promise.resolve()\n }\n\n const metadata: Record<string, unknown> =\n options?.metadata && typeof options.metadata === \"object\"\n ? { ...options.metadata }\n : {}\n\n if (typeof window !== \"undefined\") {\n metadata.pageHostname = window.location.hostname\n const params = new URLSearchParams(window.location.search)\n for (const key of [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_content\",\n \"utm_term\",\n ] as const) {\n const v = params.get(key)\n if (v && metadata[key] === undefined) {\n metadata[key] = v\n }\n }\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 })\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 getJmpTrustBundle(\n config: JmpContentConfig\n): Promise<JmpTrustBundle> {\n return getContent<JmpTrustBundle>(config, \"/api/content/trust\")\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 getTrustBundle: () => getJmpTrustBundle(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"]}
|
|
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 /**\n * Content wrappers wired in this site's template (blog, faq, trust.*, contact).\n * Reported via POST /api/content/capabilities so unused dashboard tabs stay hidden.\n * Call {@link registerJmpContentWrappers} at startup / deploy.\n */\n contentWrappers?: JmpContentWrapper[]\n /**\n * @deprecated Use `contentWrappers` (e.g. `trust.testimonials` instead of `testimonials`).\n */\n trustSlots?: JmpTrustSlot[]\n}\n\n/** Top-level and trust sub-wrappers the client can declare. */\nexport type JmpContentWrapper =\n | \"blog\"\n | \"faq\"\n | \"contact\"\n | \"trust\"\n | \"trust.summary\"\n | \"trust.testimonials\"\n | \"trust.reviews\"\n | \"trust.stats\"\n | \"trust.logos\"\n | \"trust.credentials\"\n\n/** @deprecated Trust-only shorthand; prefer `JmpContentWrapper`. */\nexport type JmpTrustSlot =\n | \"summary\"\n | \"testimonials\"\n | \"reviews\"\n | \"stats\"\n | \"logos\"\n | \"credentials\"\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 JmpTrustSummary = {\n google_rating: number | null\n google_review_count: number | null\n google_reviews_url: string | null\n yelp_url: string | null\n trustpilot_url: string | null\n disclaimer: string | null\n}\n\nexport type JmpTestimonial = {\n id: string\n slug: string\n body: string\n author_name: string\n author_descriptor: string | null\n location: string | null\n /** 1–10 (half-star steps on a 5-star scale; 10 = 5.0 stars). */\n rating: number | null\n image_url: string | null\n video_url: string | null\n /** Optional topic or service line (e.g. roofing). */\n category: string | null\n /** Site section key — filter like FAQ `category` (e.g. testimonials, reviews). */\n placement: string | null\n source: string | null\n /** Optional link to the original review on Google, Yelp, etc. */\n source_url: string | null\n featured: boolean\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustStat = {\n id: string\n slug: string\n label: string\n value: string\n sort_order: number\n updated_at: string\n}\n\nexport type JmpPartnerLogo = {\n id: string\n slug: string\n name: string\n image_url: string | null\n link_url: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpCredential = {\n id: string\n slug: string\n title: string\n issuer: string | null\n year: string | null\n image_url: string | null\n category: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustBundle = {\n summary: JmpTrustSummary\n testimonials: JmpTestimonial[]\n stats: JmpTrustStat[]\n logos: JmpPartnerLogo[]\n credentials: JmpCredential[]\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\"\nconst LANDING_REFERRER_STORAGE_KEY = \"jmp_analytics_landing_referer\"\nconst UTM_SESSION_STORAGE_KEY = \"jmp_analytics_utm\"\n\nconst UTM_PARAM_KEYS = [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_content\",\n \"utm_term\",\n] as const\n\nfunction captureAndGetLandingReferer(): string | null {\n if (typeof window === \"undefined\") {\n return null\n }\n\n try {\n const stored = sessionStorage.getItem(LANDING_REFERRER_STORAGE_KEY)\n if (stored?.trim()) {\n return stored.trim()\n }\n\n const current = document.referrer?.trim()\n if (!current) {\n return null\n }\n\n sessionStorage.setItem(LANDING_REFERRER_STORAGE_KEY, current)\n return current\n } catch {\n return document.referrer?.trim() || null\n }\n}\n\nfunction mergeSessionUtms(metadata: Record<string, unknown>) {\n if (typeof window === \"undefined\") {\n return\n }\n\n try {\n const params = new URLSearchParams(window.location.search)\n let stored: Record<string, string> = {}\n const raw = sessionStorage.getItem(UTM_SESSION_STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n stored = parsed as Record<string, string>\n }\n }\n\n let updated = false\n for (const key of UTM_PARAM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) {\n stored[key] = value\n updated = true\n }\n }\n\n if (updated) {\n sessionStorage.setItem(UTM_SESSION_STORAGE_KEY, JSON.stringify(stored))\n }\n\n for (const key of UTM_PARAM_KEYS) {\n if (metadata[key] === undefined && stored[key]) {\n metadata[key] = stored[key]\n }\n }\n } catch {\n // sessionStorage / JSON unavailable\n }\n}\n\nfunction splitPathAndSearch(pathInput: string): { pathname: string; search: string } {\n const qIndex = pathInput.indexOf(\"?\")\n if (qIndex === -1) {\n return { pathname: pathInput || \"/\", search: \"\" }\n }\n return {\n pathname: pathInput.slice(0, qIndex) || \"/\",\n search: pathInput.slice(qIndex + 1),\n }\n}\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 if (body.type === \"page_view\" && typeof window !== \"undefined\") {\n const landingReferer = captureAndGetLandingReferer()\n if (landingReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = landingReferer\n }\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 pathFromOptions = options?.path?.trim()\n let pathname = \"/\"\n let search = \"\"\n\n if (pathFromOptions) {\n const split = splitPathAndSearch(pathFromOptions)\n pathname = split.pathname\n search = split.search\n } else if (typeof window !== \"undefined\") {\n pathname = window.location.pathname\n search = window.location.search.replace(/^\\?/, \"\")\n }\n\n if (!search && typeof window !== \"undefined\") {\n search = window.location.search.replace(/^\\?/, \"\")\n }\n\n const path = search ? `${pathname}?${search}` : pathname\n\n if (shouldDedupePageView(pathname)) {\n return Promise.resolve()\n }\n\n const metadata: Record<string, unknown> =\n options?.metadata && typeof options.metadata === \"object\"\n ? { ...options.metadata }\n : {}\n\n if (typeof window !== \"undefined\") {\n metadata.pageHostname = window.location.hostname\n const params = new URLSearchParams(\n search ? `?${search}` : window.location.search\n )\n for (const key of UTM_PARAM_KEYS) {\n const v = params.get(key)\n if (v && metadata[key] === undefined) {\n metadata[key] = v\n }\n }\n mergeSessionUtms(metadata)\n const landingReferer = captureAndGetLandingReferer()\n if (landingReferer && metadata.referer === undefined) {\n metadata.referer = landingReferer\n }\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 })\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 getJmpTrustBundle(\n config: JmpContentConfig\n): Promise<JmpTrustBundle> {\n return getContent<JmpTrustBundle>(config, \"/api/content/trust\")\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\nconst registeredContentWrappers = new Set<string>()\n\nfunction resolveContentWrappersFromConfig(\n config: JmpContentConfig\n): string[] {\n if (config.contentWrappers?.length) {\n return [...config.contentWrappers]\n }\n if (config.trustSlots?.length) {\n return config.trustSlots.map((slot) =>\n `trust.${slot}`\n )\n }\n return []\n}\n\n/**\n * Tell the JMP dashboard which content wrappers this site wires (blog, faq, trust.*, contact).\n * Safe to call on every cold start; dedupes by tracking key + wrapper list.\n */\nexport async function registerJmpContentWrappers(\n config: JmpContentConfig\n): Promise<\n { ok: true; wrappers: string[] } | { ok: false; skipped: true }\n> {\n const wrappers = resolveContentWrappersFromConfig(config)\n if (!wrappers.length) {\n return { ok: false, skipped: true }\n }\n\n const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(\",\")}`\n if (registeredContentWrappers.has(dedupeKey)) {\n return { ok: true, wrappers }\n }\n\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 \"JMP content wrappers: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const url = `${baseUrl}/api/content/capabilities`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, { wrappers })\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n body: JSON.stringify({ wrappers }),\n })\n\n const text = await res.text()\n if (!res.ok) {\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 throw new Error(\n [`JMP content wrappers HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n registeredContentWrappers.add(dedupeKey)\n const data = JSON.parse(text) as { wrappers?: string[] }\n return { ok: true, wrappers: data.wrappers ?? wrappers }\n}\n\n/** @deprecated Use {@link registerJmpContentWrappers} with `contentWrappers`. */\nexport async function registerJmpTrustCapabilities(\n config: JmpContentConfig\n): Promise<{ ok: true; slots: JmpTrustSlot[] } | { ok: false; skipped: true }> {\n const result = await registerJmpContentWrappers(config)\n if (!result.ok) {\n return result\n }\n const slots = result.wrappers\n .filter((w) => w === \"trust\" || w.startsWith(\"trust.\"))\n .map((w) => (w === \"trust\" ? \"summary\" : w.slice(\"trust.\".length)))\n .filter((s): s is JmpTrustSlot =>\n [\"summary\", \"testimonials\", \"reviews\", \"stats\", \"logos\", \"credentials\"].includes(\n s\n )\n )\n return { ok: true, slots }\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 getTrustBundle: () => getJmpTrustBundle(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n registerContentWrappers: () => registerJmpContentWrappers(config),\n /** @deprecated Use registerContentWrappers */\n registerTrustCapabilities: () => registerJmpTrustCapabilities(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.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
JmpTrackedMailtoLink,
|
|
4
4
|
JmpTrackedTelLink,
|
|
5
5
|
useJmpAnalytics
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZLWI2UAP.js";
|
|
7
7
|
import {
|
|
8
8
|
getOrCreateJmpAnalyticsFromBrowserEnv
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ST5BERIA.js";
|
|
10
10
|
|
|
11
11
|
// src/next.tsx
|
|
12
12
|
import { Suspense, useEffect } from "react";
|
package/dist/react.cjs
CHANGED
|
@@ -31,6 +31,77 @@ var import_react = require("react");
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var DEFAULT_SOURCE = "direct";
|
|
33
33
|
var SESSION_STORAGE_KEY = "jmp_analytics_sid";
|
|
34
|
+
var LANDING_REFERRER_STORAGE_KEY = "jmp_analytics_landing_referer";
|
|
35
|
+
var UTM_SESSION_STORAGE_KEY = "jmp_analytics_utm";
|
|
36
|
+
var UTM_PARAM_KEYS = [
|
|
37
|
+
"utm_source",
|
|
38
|
+
"utm_medium",
|
|
39
|
+
"utm_campaign",
|
|
40
|
+
"utm_content",
|
|
41
|
+
"utm_term"
|
|
42
|
+
];
|
|
43
|
+
function captureAndGetLandingReferer() {
|
|
44
|
+
if (typeof window === "undefined") {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const stored = sessionStorage.getItem(LANDING_REFERRER_STORAGE_KEY);
|
|
49
|
+
if (stored?.trim()) {
|
|
50
|
+
return stored.trim();
|
|
51
|
+
}
|
|
52
|
+
const current = document.referrer?.trim();
|
|
53
|
+
if (!current) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
sessionStorage.setItem(LANDING_REFERRER_STORAGE_KEY, current);
|
|
57
|
+
return current;
|
|
58
|
+
} catch {
|
|
59
|
+
return document.referrer?.trim() || null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function mergeSessionUtms(metadata) {
|
|
63
|
+
if (typeof window === "undefined") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const params = new URLSearchParams(window.location.search);
|
|
68
|
+
let stored = {};
|
|
69
|
+
const raw = sessionStorage.getItem(UTM_SESSION_STORAGE_KEY);
|
|
70
|
+
if (raw) {
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
73
|
+
stored = parsed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let updated = false;
|
|
77
|
+
for (const key of UTM_PARAM_KEYS) {
|
|
78
|
+
const value = params.get(key)?.trim();
|
|
79
|
+
if (value) {
|
|
80
|
+
stored[key] = value;
|
|
81
|
+
updated = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (updated) {
|
|
85
|
+
sessionStorage.setItem(UTM_SESSION_STORAGE_KEY, JSON.stringify(stored));
|
|
86
|
+
}
|
|
87
|
+
for (const key of UTM_PARAM_KEYS) {
|
|
88
|
+
if (metadata[key] === void 0 && stored[key]) {
|
|
89
|
+
metadata[key] = stored[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function splitPathAndSearch(pathInput) {
|
|
96
|
+
const qIndex = pathInput.indexOf("?");
|
|
97
|
+
if (qIndex === -1) {
|
|
98
|
+
return { pathname: pathInput || "/", search: "" };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
pathname: pathInput.slice(0, qIndex) || "/",
|
|
102
|
+
search: pathInput.slice(qIndex + 1)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
34
105
|
function responseLooksLikeHtml(body) {
|
|
35
106
|
const s = body.trimStart();
|
|
36
107
|
return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
|
|
@@ -76,6 +147,12 @@ function createJmpAnalytics(config) {
|
|
|
76
147
|
"Content-Type": "application/json",
|
|
77
148
|
"X-JMP-Tracking-Key": config.trackingKey
|
|
78
149
|
};
|
|
150
|
+
if (body.type === "page_view" && typeof window !== "undefined") {
|
|
151
|
+
const landingReferer = captureAndGetLandingReferer();
|
|
152
|
+
if (landingReferer) {
|
|
153
|
+
headers["X-JMP-Visitor-Referer"] = landingReferer;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
79
156
|
const sid = getOrCreateSessionId();
|
|
80
157
|
const payload = { ...body, ...sid ? { sessionId: sid } : {} };
|
|
81
158
|
if (config.debug) {
|
|
@@ -182,8 +259,20 @@ function createJmpAnalytics(config) {
|
|
|
182
259
|
return false;
|
|
183
260
|
}
|
|
184
261
|
function trackPageView(options) {
|
|
185
|
-
const
|
|
186
|
-
|
|
262
|
+
const pathFromOptions = options?.path?.trim();
|
|
263
|
+
let pathname = "/";
|
|
264
|
+
let search = "";
|
|
265
|
+
if (pathFromOptions) {
|
|
266
|
+
const split = splitPathAndSearch(pathFromOptions);
|
|
267
|
+
pathname = split.pathname;
|
|
268
|
+
search = split.search;
|
|
269
|
+
} else if (typeof window !== "undefined") {
|
|
270
|
+
pathname = window.location.pathname;
|
|
271
|
+
search = window.location.search.replace(/^\?/, "");
|
|
272
|
+
}
|
|
273
|
+
if (!search && typeof window !== "undefined") {
|
|
274
|
+
search = window.location.search.replace(/^\?/, "");
|
|
275
|
+
}
|
|
187
276
|
const path = search ? `${pathname}?${search}` : pathname;
|
|
188
277
|
if (shouldDedupePageView(pathname)) {
|
|
189
278
|
return Promise.resolve();
|
|
@@ -191,19 +280,20 @@ function createJmpAnalytics(config) {
|
|
|
191
280
|
const metadata = options?.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
192
281
|
if (typeof window !== "undefined") {
|
|
193
282
|
metadata.pageHostname = window.location.hostname;
|
|
194
|
-
const params = new URLSearchParams(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"utm_campaign",
|
|
199
|
-
"utm_content",
|
|
200
|
-
"utm_term"
|
|
201
|
-
]) {
|
|
283
|
+
const params = new URLSearchParams(
|
|
284
|
+
search ? `?${search}` : window.location.search
|
|
285
|
+
);
|
|
286
|
+
for (const key of UTM_PARAM_KEYS) {
|
|
202
287
|
const v = params.get(key);
|
|
203
288
|
if (v && metadata[key] === void 0) {
|
|
204
289
|
metadata[key] = v;
|
|
205
290
|
}
|
|
206
291
|
}
|
|
292
|
+
mergeSessionUtms(metadata);
|
|
293
|
+
const landingReferer = captureAndGetLandingReferer();
|
|
294
|
+
if (landingReferer && metadata.referer === void 0) {
|
|
295
|
+
metadata.referer = landingReferer;
|
|
296
|
+
}
|
|
207
297
|
}
|
|
208
298
|
return send({
|
|
209
299
|
type: "page_view",
|
package/dist/react.cjs.map
CHANGED
|
@@ -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\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 JmpTrustSummary = {\n google_rating: number | null\n google_review_count: number | null\n google_reviews_url: string | null\n yelp_url: string | null\n trustpilot_url: string | null\n disclaimer: string | null\n}\n\nexport type JmpTestimonial = {\n id: string\n slug: string\n body: string\n author_name: string\n author_descriptor: string | null\n location: string | null\n rating: number | null\n image_url: string | null\n video_url: string | null\n category: string | null\n source: string | null\n featured: boolean\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustStat = {\n id: string\n slug: string\n label: string\n value: string\n sort_order: number\n updated_at: string\n}\n\nexport type JmpPartnerLogo = {\n id: string\n slug: string\n name: string\n image_url: string | null\n link_url: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpCredential = {\n id: string\n slug: string\n title: string\n issuer: string | null\n year: string | null\n image_url: string | null\n category: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustBundle = {\n summary: JmpTrustSummary\n testimonials: JmpTestimonial[]\n stats: JmpTrustStat[]\n logos: JmpPartnerLogo[]\n credentials: JmpCredential[]\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 pathname =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n const search =\n typeof window !== \"undefined\" ? window.location.search.replace(/^\\?/, \"\") : \"\"\n const path = search ? `${pathname}?${search}` : pathname\n\n if (shouldDedupePageView(pathname)) {\n return Promise.resolve()\n }\n\n const metadata: Record<string, unknown> =\n options?.metadata && typeof options.metadata === \"object\"\n ? { ...options.metadata }\n : {}\n\n if (typeof window !== \"undefined\") {\n metadata.pageHostname = window.location.hostname\n const params = new URLSearchParams(window.location.search)\n for (const key of [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_content\",\n \"utm_term\",\n ] as const) {\n const v = params.get(key)\n if (v && metadata[key] === undefined) {\n metadata[key] = v\n }\n }\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 })\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 getJmpTrustBundle(\n config: JmpContentConfig\n): Promise<JmpTrustBundle> {\n return getContent<JmpTrustBundle>(config, \"/api/content/trust\")\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 getTrustBundle: () => getJmpTrustBundle(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 /**\n * Content wrappers wired in this site's template (blog, faq, trust.*, contact).\n * Reported via POST /api/content/capabilities so unused dashboard tabs stay hidden.\n * Call {@link registerJmpContentWrappers} at startup / deploy.\n */\n contentWrappers?: JmpContentWrapper[]\n /**\n * @deprecated Use `contentWrappers` (e.g. `trust.testimonials` instead of `testimonials`).\n */\n trustSlots?: JmpTrustSlot[]\n}\n\n/** Top-level and trust sub-wrappers the client can declare. */\nexport type JmpContentWrapper =\n | \"blog\"\n | \"faq\"\n | \"contact\"\n | \"trust\"\n | \"trust.summary\"\n | \"trust.testimonials\"\n | \"trust.reviews\"\n | \"trust.stats\"\n | \"trust.logos\"\n | \"trust.credentials\"\n\n/** @deprecated Trust-only shorthand; prefer `JmpContentWrapper`. */\nexport type JmpTrustSlot =\n | \"summary\"\n | \"testimonials\"\n | \"reviews\"\n | \"stats\"\n | \"logos\"\n | \"credentials\"\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 JmpTrustSummary = {\n google_rating: number | null\n google_review_count: number | null\n google_reviews_url: string | null\n yelp_url: string | null\n trustpilot_url: string | null\n disclaimer: string | null\n}\n\nexport type JmpTestimonial = {\n id: string\n slug: string\n body: string\n author_name: string\n author_descriptor: string | null\n location: string | null\n /** 1–10 (half-star steps on a 5-star scale; 10 = 5.0 stars). */\n rating: number | null\n image_url: string | null\n video_url: string | null\n /** Optional topic or service line (e.g. roofing). */\n category: string | null\n /** Site section key — filter like FAQ `category` (e.g. testimonials, reviews). */\n placement: string | null\n source: string | null\n /** Optional link to the original review on Google, Yelp, etc. */\n source_url: string | null\n featured: boolean\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustStat = {\n id: string\n slug: string\n label: string\n value: string\n sort_order: number\n updated_at: string\n}\n\nexport type JmpPartnerLogo = {\n id: string\n slug: string\n name: string\n image_url: string | null\n link_url: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpCredential = {\n id: string\n slug: string\n title: string\n issuer: string | null\n year: string | null\n image_url: string | null\n category: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpTrustBundle = {\n summary: JmpTrustSummary\n testimonials: JmpTestimonial[]\n stats: JmpTrustStat[]\n logos: JmpPartnerLogo[]\n credentials: JmpCredential[]\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\"\nconst LANDING_REFERRER_STORAGE_KEY = \"jmp_analytics_landing_referer\"\nconst UTM_SESSION_STORAGE_KEY = \"jmp_analytics_utm\"\n\nconst UTM_PARAM_KEYS = [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_content\",\n \"utm_term\",\n] as const\n\nfunction captureAndGetLandingReferer(): string | null {\n if (typeof window === \"undefined\") {\n return null\n }\n\n try {\n const stored = sessionStorage.getItem(LANDING_REFERRER_STORAGE_KEY)\n if (stored?.trim()) {\n return stored.trim()\n }\n\n const current = document.referrer?.trim()\n if (!current) {\n return null\n }\n\n sessionStorage.setItem(LANDING_REFERRER_STORAGE_KEY, current)\n return current\n } catch {\n return document.referrer?.trim() || null\n }\n}\n\nfunction mergeSessionUtms(metadata: Record<string, unknown>) {\n if (typeof window === \"undefined\") {\n return\n }\n\n try {\n const params = new URLSearchParams(window.location.search)\n let stored: Record<string, string> = {}\n const raw = sessionStorage.getItem(UTM_SESSION_STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n stored = parsed as Record<string, string>\n }\n }\n\n let updated = false\n for (const key of UTM_PARAM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) {\n stored[key] = value\n updated = true\n }\n }\n\n if (updated) {\n sessionStorage.setItem(UTM_SESSION_STORAGE_KEY, JSON.stringify(stored))\n }\n\n for (const key of UTM_PARAM_KEYS) {\n if (metadata[key] === undefined && stored[key]) {\n metadata[key] = stored[key]\n }\n }\n } catch {\n // sessionStorage / JSON unavailable\n }\n}\n\nfunction splitPathAndSearch(pathInput: string): { pathname: string; search: string } {\n const qIndex = pathInput.indexOf(\"?\")\n if (qIndex === -1) {\n return { pathname: pathInput || \"/\", search: \"\" }\n }\n return {\n pathname: pathInput.slice(0, qIndex) || \"/\",\n search: pathInput.slice(qIndex + 1),\n }\n}\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 if (body.type === \"page_view\" && typeof window !== \"undefined\") {\n const landingReferer = captureAndGetLandingReferer()\n if (landingReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = landingReferer\n }\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 pathFromOptions = options?.path?.trim()\n let pathname = \"/\"\n let search = \"\"\n\n if (pathFromOptions) {\n const split = splitPathAndSearch(pathFromOptions)\n pathname = split.pathname\n search = split.search\n } else if (typeof window !== \"undefined\") {\n pathname = window.location.pathname\n search = window.location.search.replace(/^\\?/, \"\")\n }\n\n if (!search && typeof window !== \"undefined\") {\n search = window.location.search.replace(/^\\?/, \"\")\n }\n\n const path = search ? `${pathname}?${search}` : pathname\n\n if (shouldDedupePageView(pathname)) {\n return Promise.resolve()\n }\n\n const metadata: Record<string, unknown> =\n options?.metadata && typeof options.metadata === \"object\"\n ? { ...options.metadata }\n : {}\n\n if (typeof window !== \"undefined\") {\n metadata.pageHostname = window.location.hostname\n const params = new URLSearchParams(\n search ? `?${search}` : window.location.search\n )\n for (const key of UTM_PARAM_KEYS) {\n const v = params.get(key)\n if (v && metadata[key] === undefined) {\n metadata[key] = v\n }\n }\n mergeSessionUtms(metadata)\n const landingReferer = captureAndGetLandingReferer()\n if (landingReferer && metadata.referer === undefined) {\n metadata.referer = landingReferer\n }\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 })\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 getJmpTrustBundle(\n config: JmpContentConfig\n): Promise<JmpTrustBundle> {\n return getContent<JmpTrustBundle>(config, \"/api/content/trust\")\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\nconst registeredContentWrappers = new Set<string>()\n\nfunction resolveContentWrappersFromConfig(\n config: JmpContentConfig\n): string[] {\n if (config.contentWrappers?.length) {\n return [...config.contentWrappers]\n }\n if (config.trustSlots?.length) {\n return config.trustSlots.map((slot) =>\n `trust.${slot}`\n )\n }\n return []\n}\n\n/**\n * Tell the JMP dashboard which content wrappers this site wires (blog, faq, trust.*, contact).\n * Safe to call on every cold start; dedupes by tracking key + wrapper list.\n */\nexport async function registerJmpContentWrappers(\n config: JmpContentConfig\n): Promise<\n { ok: true; wrappers: string[] } | { ok: false; skipped: true }\n> {\n const wrappers = resolveContentWrappersFromConfig(config)\n if (!wrappers.length) {\n return { ok: false, skipped: true }\n }\n\n const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(\",\")}`\n if (registeredContentWrappers.has(dedupeKey)) {\n return { ok: true, wrappers }\n }\n\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 \"JMP content wrappers: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const url = `${baseUrl}/api/content/capabilities`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, { wrappers })\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n body: JSON.stringify({ wrappers }),\n })\n\n const text = await res.text()\n if (!res.ok) {\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 throw new Error(\n [`JMP content wrappers HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n registeredContentWrappers.add(dedupeKey)\n const data = JSON.parse(text) as { wrappers?: string[] }\n return { ok: true, wrappers: data.wrappers ?? wrappers }\n}\n\n/** @deprecated Use {@link registerJmpContentWrappers} with `contentWrappers`. */\nexport async function registerJmpTrustCapabilities(\n config: JmpContentConfig\n): Promise<{ ok: true; slots: JmpTrustSlot[] } | { ok: false; skipped: true }> {\n const result = await registerJmpContentWrappers(config)\n if (!result.ok) {\n return result\n }\n const slots = result.wrappers\n .filter((w) => w === \"trust\" || w.startsWith(\"trust.\"))\n .map((w) => (w === \"trust\" ? \"summary\" : w.slice(\"trust.\".length)))\n .filter((s): s is JmpTrustSlot =>\n [\"summary\", \"testimonials\", \"reviews\", \"stats\", \"logos\", \"credentials\"].includes(\n s\n )\n )\n return { ok: true, slots }\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 getTrustBundle: () => getJmpTrustBundle(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n registerContentWrappers: () => registerJmpContentWrappers(config),\n /** @deprecated Use registerContentWrappers */\n registerTrustCapabilities: () => registerJmpTrustCapabilities(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.js
CHANGED
package/package.json
CHANGED