@jmp-technologies/analytics 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ content helpers for client websites.
7
7
 
8
8
  1. Deploy **JMP Analytics** (jmp-dashboard) to a stable **HTTPS** URL.
9
9
  2. Create a **website** row (domain + **tracking_key**) and assign it to the client user.
10
- 3. On the client site: **`npm install @jmp-technologies/analytics`**, set **`NEXT_PUBLIC_JMP_ANALYTICS_URL`** and **`JMP_TRACKING_KEY`**, wire **`trackPageView`** (and optional events).
10
+ 3. On the client site: **`npm install @jmp-technologies/analytics`**, set **`NEXT_PUBLIC_JMP_ANALYTICS_URL`** and **`NEXT_PUBLIC_JMP_TRACKING_KEY`** (same key for browser and server in Next.js), wire **`trackPageView`** (and optional events).
11
11
  4. Verify **`POST /api/events`** returns **201** and data appears in the dashboard.
12
12
 
13
13
  **Step-by-step checklist:** [Client onboarding](https://github.com/jmp-technologies/jmp-dashboard/blob/main/docs/client-onboarding.md)
@@ -22,19 +22,89 @@ If your GitHub org/repo name differs, replace `jmp-technologies/jmp-dashboard` i
22
22
 
23
23
  From this repo, the same files are at **`docs/client-onboarding.md`** and **`docs/tracking-integration.md`**.
24
24
 
25
+ ## Quick setup (env helpers)
26
+
27
+ Set **`NEXT_PUBLIC_JMP_ANALYTICS_URL`** and **`NEXT_PUBLIC_JMP_TRACKING_KEY`** (used for browser tracking, server content fetches, and `getJmpServerContentConfigFromEnv`).
28
+
29
+ ### Next.js App Router (least client code)
30
+
31
+ 1. Install peers: **`react`**, **`react-dom`**, **`next`** (already present in a Next app).
32
+ 2. In **`app/layout.tsx`** (or the root layout that wraps all pages), add **one** client line:
33
+
34
+ ```tsx
35
+ import type { ReactNode } from "react"
36
+ import { JmpNextAnalytics } from "@jmp-technologies/analytics/next"
37
+
38
+ export default function RootLayout({ children }: { children: ReactNode }) {
39
+ return (
40
+ <html lang="en">
41
+ <body>
42
+ <JmpNextAnalytics />
43
+ {children}
44
+ </body>
45
+ </html>
46
+ )
47
+ }
48
+ ```
49
+
50
+ That enables **automatic `page_view`** on first load and on client-side navigations (includes query string). No `jmp-analytics-client.ts`, no `useEffect` + `usePathname` boilerplate.
51
+
52
+ **Optional:** track phone clicks without custom handlers — use **`JmpTrackedTelLink`** from the same module:
53
+
54
+ ```tsx
55
+ import { JmpNextAnalytics, JmpTrackedTelLink } from "@jmp-technologies/analytics/next"
56
+
57
+ <JmpTrackedTelLink href="tel:+15551234567" className="...">
58
+ Call us
59
+ </JmpTrackedTelLink>
60
+ ```
61
+
62
+ **Hooks:** `useJmpAnalytics()` from `@jmp-technologies/analytics/next` or `@jmp-technologies/analytics/react` for forms and custom events.
63
+
64
+ ---
65
+
66
+ ### Manual browser (any framework)
67
+
68
+ **Browser (client components)** — one module-level singleton per tab, default `pageViewDedupeMs` (800):
69
+
70
+ ```ts
71
+ import { getOrCreateJmpAnalyticsFromBrowserEnv } from "@jmp-technologies/analytics"
72
+
73
+ const analytics = getOrCreateJmpAnalyticsFromBrowserEnv()
74
+ void analytics?.trackPageView()
75
+ ```
76
+
77
+ **Server** — blog / FAQ / content API:
78
+
79
+ ```tsx
80
+ import { getJmpBlogPosts, getJmpServerContentConfigFromEnv } from "@jmp-technologies/analytics"
81
+
82
+ export default async function BlogPage() {
83
+ const config = getJmpServerContentConfigFromEnv()
84
+ if (!config) return null
85
+ const posts = await getJmpBlogPosts(config)
86
+ // ...
87
+ }
88
+ ```
89
+
90
+ Or **`createJmpContentClientFromServerEnv()`** for `getBlogPosts()` / `getBlogPost()` / `getFaqs()` in one object.
91
+
92
+ **Configured?** `isJmpBrowserAnalyticsEnvConfigured()` and `isJmpServerContentEnvConfigured()`.
93
+
94
+ ---
95
+
25
96
  ## Blog and FAQ content
26
97
 
27
98
  Manage content in the JMP dashboard at `/content`, then render it on the
28
99
  client website from the client site's own routes:
29
100
 
30
101
  ```tsx
31
- import { getJmpBlogPosts } from "@jmp-technologies/analytics"
102
+ import { getJmpBlogPosts, getJmpServerContentConfigFromEnv } from "@jmp-technologies/analytics"
32
103
 
33
104
  export default async function BlogPage() {
34
- const posts = await getJmpBlogPosts({
35
- baseUrl: process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL!,
36
- trackingKey: process.env.JMP_TRACKING_KEY!,
37
- })
105
+ const config = getJmpServerContentConfigFromEnv()
106
+ if (!config) return null
107
+ const posts = await getJmpBlogPosts(config)
38
108
 
39
109
  return (
40
110
  <main>
@@ -52,6 +122,27 @@ export default async function BlogPage() {
52
122
 
53
123
  Available helpers:
54
124
 
125
+ FAQ items include a stable `slug` field. Use that slug, rather than the `id`,
126
+ when building FAQ URLs so tracked page views are stored as readable paths.
127
+
128
+ **Config from env**
129
+
130
+ - `getJmpBrowserAnalyticsConfigFromEnv()` / `getJmpServerContentConfigFromEnv()`
131
+ - `getOrCreateJmpAnalyticsFromBrowserEnv()` (browser singleton)
132
+ - `createJmpContentClientFromServerEnv()`
133
+ - `isJmpBrowserAnalyticsEnvConfigured()` / `isJmpServerContentEnvConfigured()`
134
+
135
+ **Next.js (`@jmp-technologies/analytics/next`)**
136
+
137
+ - `JmpNextAnalytics` / `JmpNextPageViewTracker` — App Router page views
138
+ - Re-exports: `JmpTrackedTelLink`, `useJmpAnalytics`
139
+
140
+ **React (`@jmp-technologies/analytics/react`)**
141
+
142
+ - `JmpTrackedTelLink`, `useJmpAnalytics` — Vite, CRA, etc. (no automatic route tracking; call `trackPageView` yourself or use your router)
143
+
144
+ **Content**
145
+
55
146
  - `getJmpBlogPosts(config)`
56
147
  - `getJmpBlogPost(config, slug)`
57
148
  - `getJmpFaqs(config)`
@@ -0,0 +1,35 @@
1
+ import {
2
+ getOrCreateJmpAnalyticsFromBrowserEnv
3
+ } from "./chunk-RYFQXTZ7.js";
4
+
5
+ // src/react.tsx
6
+ import { useMemo } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ function useJmpAnalytics() {
9
+ return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), []);
10
+ }
11
+ function JmpTrackedTelLink({
12
+ href,
13
+ onClick,
14
+ ...rest
15
+ }) {
16
+ return /* @__PURE__ */ jsx(
17
+ "a",
18
+ {
19
+ href,
20
+ ...rest,
21
+ onClick: (e) => {
22
+ const client = getOrCreateJmpAnalyticsFromBrowserEnv();
23
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
24
+ void client?.trackCallClick(path);
25
+ onClick?.(e);
26
+ }
27
+ }
28
+ );
29
+ }
30
+
31
+ export {
32
+ useJmpAnalytics,
33
+ JmpTrackedTelLink
34
+ };
35
+ //# sourceMappingURL=chunk-O6K2SIUM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.tsx"],"sourcesContent":["\"use client\"\n\nimport { useMemo, type ComponentPropsWithoutRef } from \"react\"\nimport type { JmpAnalytics } from \"./index\"\nimport { getOrCreateJmpAnalyticsFromBrowserEnv } from \"./index\"\n\nexport function useJmpAnalytics(): JmpAnalytics | null {\n return useMemo(() => getOrCreateJmpAnalyticsFromBrowserEnv(), [])\n}\n\nexport type JmpTrackedTelLinkProps = ComponentPropsWithoutRef<\"a\"> & {\n href: string\n}\n\n/**\n * `tel:` (or other) link that records a `call_click` with the current path.\n */\nexport function JmpTrackedTelLink({\n href,\n onClick,\n ...rest\n}: JmpTrackedTelLinkProps) {\n return (\n <a\n href={href}\n {...rest}\n onClick={(e) => {\n const client = getOrCreateJmpAnalyticsFromBrowserEnv()\n const path =\n typeof window !== \"undefined\" ? window.location.pathname : \"/\"\n void client?.trackCallClick(path)\n onClick?.(e)\n }}\n />\n )\n}\n"],"mappings":";;;;;AAEA,SAAS,eAA8C;AAqBnD;AAjBG,SAAS,kBAAuC;AACrD,SAAO,QAAQ,MAAM,sCAAsC,GAAG,CAAC,CAAC;AAClE;AASO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2B;AACzB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,SAAS,CAAC,MAAM;AACd,cAAM,SAAS,sCAAsC;AACrD,cAAM,OACJ,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAC7D,aAAK,QAAQ,eAAe,IAAI;AAChC,kBAAU,CAAC;AAAA,MACb;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,388 @@
1
+ // src/index.ts
2
+ var DEFAULT_SOURCE = "direct";
3
+ var SESSION_STORAGE_KEY = "jmp_analytics_sid";
4
+ function responseLooksLikeHtml(body) {
5
+ const s = body.trimStart();
6
+ return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
7
+ }
8
+ function normalizeBaseUrl(baseUrl) {
9
+ return baseUrl.replace(/\/$/, "");
10
+ }
11
+ function inferDevice() {
12
+ if (typeof window === "undefined") {
13
+ return "unknown";
14
+ }
15
+ return window.innerWidth < 768 ? "mobile" : "desktop";
16
+ }
17
+ function getOrCreateSessionId() {
18
+ if (typeof window === "undefined" || !globalThis.crypto?.randomUUID) {
19
+ return void 0;
20
+ }
21
+ try {
22
+ let id = sessionStorage.getItem(SESSION_STORAGE_KEY);
23
+ if (!id) {
24
+ id = crypto.randomUUID();
25
+ sessionStorage.setItem(SESSION_STORAGE_KEY, id);
26
+ }
27
+ return id;
28
+ } catch {
29
+ return void 0;
30
+ }
31
+ }
32
+ function createJmpAnalytics(config) {
33
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
34
+ const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
35
+ if (!resolvedFetch) {
36
+ throw new Error(
37
+ "createJmpAnalytics: fetch is not available; pass fetchImpl in config"
38
+ );
39
+ }
40
+ const fetchFn = resolvedFetch;
41
+ let lastDedupePath = "";
42
+ let lastDedupeAt = 0;
43
+ async function send(body) {
44
+ const url = `${baseUrl}/api/events`;
45
+ const headers = {
46
+ "Content-Type": "application/json",
47
+ "X-JMP-Tracking-Key": config.trackingKey
48
+ };
49
+ const sid = getOrCreateSessionId();
50
+ const payload = { ...body, ...sid ? { sessionId: sid } : {} };
51
+ if (config.debug) {
52
+ console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
53
+ }
54
+ const res = await fetchFn(url, {
55
+ method: "POST",
56
+ headers,
57
+ body: JSON.stringify(payload),
58
+ keepalive: true
59
+ });
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ if (responseLooksLikeHtml(text)) {
63
+ throw new Error(
64
+ `JMP Analytics HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), not a marketing or client site unless that codebase ships the same API route.`
65
+ );
66
+ }
67
+ let parsed = {};
68
+ try {
69
+ parsed = JSON.parse(text);
70
+ } catch {
71
+ parsed = { error: text };
72
+ }
73
+ const detail = parsed.error ?? text;
74
+ const segments = [
75
+ `JMP Analytics HTTP ${res.status}`,
76
+ parsed.code ? `[${parsed.code}]` : null,
77
+ detail,
78
+ parsed.hint ? `Hint: ${parsed.hint}` : null,
79
+ parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
80
+ ].filter(Boolean);
81
+ const message = segments.join(" \u2014 ");
82
+ if (config.debug) {
83
+ console.debug("[@jmp-technologies/analytics]", "error response", {
84
+ status: res.status,
85
+ ...parsed,
86
+ raw: text
87
+ });
88
+ }
89
+ throw new Error(message);
90
+ }
91
+ }
92
+ async function sendLead(body) {
93
+ const url = `${baseUrl}/api/leads`;
94
+ const headers = {
95
+ "Content-Type": "application/json",
96
+ "X-JMP-Tracking-Key": config.trackingKey
97
+ };
98
+ const sid = getOrCreateSessionId();
99
+ const payload = { ...body, ...sid ? { sessionId: sid } : {} };
100
+ if (config.debug) {
101
+ console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
102
+ }
103
+ const res = await fetchFn(url, {
104
+ method: "POST",
105
+ headers,
106
+ body: JSON.stringify(payload),
107
+ keepalive: true
108
+ });
109
+ if (!res.ok) {
110
+ const text = await res.text();
111
+ if (responseLooksLikeHtml(text)) {
112
+ throw new Error(
113
+ `JMP Analytics HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`
114
+ );
115
+ }
116
+ let parsed = {};
117
+ try {
118
+ parsed = JSON.parse(text);
119
+ } catch {
120
+ parsed = { error: text };
121
+ }
122
+ const detail = parsed.error ?? text;
123
+ const segments = [
124
+ `JMP Analytics HTTP ${res.status}`,
125
+ parsed.code ? `[${parsed.code}]` : null,
126
+ detail,
127
+ parsed.hint ? `Hint: ${parsed.hint}` : null,
128
+ parsed.expectedDomain ? `Domain in JMP admin: ${parsed.expectedDomain}` : null
129
+ ].filter(Boolean);
130
+ const message = segments.join(" \u2014 ");
131
+ if (config.debug) {
132
+ console.debug("[@jmp-technologies/analytics]", "error response", {
133
+ status: res.status,
134
+ ...parsed,
135
+ raw: text
136
+ });
137
+ }
138
+ throw new Error(message);
139
+ }
140
+ }
141
+ function shouldDedupePageView(path) {
142
+ const ms = config.pageViewDedupeMs ?? 0;
143
+ if (ms <= 0 || typeof window === "undefined") {
144
+ return false;
145
+ }
146
+ const now = Date.now();
147
+ if (path === lastDedupePath && now - lastDedupeAt < ms) {
148
+ return true;
149
+ }
150
+ lastDedupePath = path;
151
+ lastDedupeAt = now;
152
+ return false;
153
+ }
154
+ function trackPageView(options) {
155
+ const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
156
+ if (shouldDedupePageView(path)) {
157
+ return Promise.resolve();
158
+ }
159
+ return send({
160
+ type: "page_view",
161
+ path: path || "/",
162
+ source: options?.source ?? DEFAULT_SOURCE,
163
+ device: options?.device ?? inferDevice(),
164
+ country: options?.country,
165
+ metadata: options?.metadata && typeof options.metadata === "object" ? options.metadata : {}
166
+ });
167
+ }
168
+ function trackFormSubmit(path, metadata) {
169
+ return send({
170
+ type: "form_submit",
171
+ path: path || "/",
172
+ metadata: metadata ?? {}
173
+ });
174
+ }
175
+ function trackCallClick(path, metadata) {
176
+ return send({
177
+ type: "call_click",
178
+ path: path || "/",
179
+ metadata: metadata ?? {}
180
+ });
181
+ }
182
+ function trackLead(options) {
183
+ const path = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
184
+ const body = { path: path || "/" };
185
+ if (options?.name) body.name = options.name;
186
+ if (options?.email) body.email = options.email;
187
+ if (options?.phone) body.phone = options.phone;
188
+ if (options?.company) body.company = options.company;
189
+ if (options?.website) body.website = options.website;
190
+ if (options?.subject) body.subject = options.subject;
191
+ if (options?.message) body.message = options.message;
192
+ if (options?.metadata && typeof options.metadata === "object") {
193
+ body.metadata = options.metadata;
194
+ }
195
+ if (options?.sessionId) body.sessionId = options.sessionId;
196
+ return sendLead(body);
197
+ }
198
+ function track(type, payload) {
199
+ if (type === "page_view") {
200
+ return trackPageView({
201
+ path: payload.path,
202
+ source: payload.source,
203
+ device: payload.device,
204
+ country: payload.country,
205
+ metadata: payload.metadata
206
+ });
207
+ }
208
+ if (type === "form_submit") {
209
+ return trackFormSubmit(payload.path, payload.metadata);
210
+ }
211
+ return trackCallClick(payload.path, payload.metadata);
212
+ }
213
+ function subscribeToSpaNavigation() {
214
+ if (typeof window === "undefined") {
215
+ return () => {
216
+ };
217
+ }
218
+ let lastPath = window.location.pathname;
219
+ const run = () => {
220
+ const path = window.location.pathname;
221
+ if (path !== lastPath) {
222
+ lastPath = path;
223
+ void trackPageView({ path });
224
+ }
225
+ };
226
+ window.addEventListener("popstate", run);
227
+ const push = history.pushState.bind(history);
228
+ const replace = history.replaceState.bind(history);
229
+ history.pushState = (...args) => {
230
+ const r = push(...args);
231
+ queueMicrotask(run);
232
+ return r;
233
+ };
234
+ history.replaceState = (...args) => {
235
+ const r = replace(...args);
236
+ queueMicrotask(run);
237
+ return r;
238
+ };
239
+ return () => {
240
+ window.removeEventListener("popstate", run);
241
+ history.pushState = push;
242
+ history.replaceState = replace;
243
+ };
244
+ }
245
+ return {
246
+ track,
247
+ trackPageView,
248
+ trackFormSubmit,
249
+ trackCallClick,
250
+ trackLead,
251
+ subscribeToSpaNavigation
252
+ };
253
+ }
254
+ async function getJmpBlogPosts(config) {
255
+ const data = await getContent(
256
+ config,
257
+ "/api/content/blog"
258
+ );
259
+ return data.posts;
260
+ }
261
+ async function getJmpBlogPost(config, slug) {
262
+ try {
263
+ const data = await getContent(
264
+ config,
265
+ `/api/content/blog/${encodeURIComponent(slug)}`
266
+ );
267
+ return data.post;
268
+ } catch (error) {
269
+ if (error instanceof Error && error.message.includes("HTTP 404")) {
270
+ return null;
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+ async function getJmpFaqs(config) {
276
+ const data = await getContent(
277
+ config,
278
+ "/api/content/faqs"
279
+ );
280
+ return data.faqs;
281
+ }
282
+ function createJmpContentClient(config) {
283
+ return {
284
+ getBlogPosts: () => getJmpBlogPosts(config),
285
+ getBlogPost: (slug) => getJmpBlogPost(config, slug),
286
+ getFaqs: () => getJmpFaqs(config)
287
+ };
288
+ }
289
+ async function getContent(config, path) {
290
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
291
+ const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
292
+ if (!resolvedFetch) {
293
+ throw new Error("JMP content: fetch is not available; pass fetchImpl in config");
294
+ }
295
+ const url = `${baseUrl}${path}`;
296
+ if (config.debug) {
297
+ console.debug("[@jmp-technologies/analytics]", "GET", url);
298
+ }
299
+ const res = await resolvedFetch(url, {
300
+ method: "GET",
301
+ headers: {
302
+ "X-JMP-Tracking-Key": config.trackingKey
303
+ }
304
+ });
305
+ const text = await res.text();
306
+ if (!res.ok) {
307
+ if (responseLooksLikeHtml(text)) {
308
+ throw new Error(
309
+ `JMP content HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl to your JMP Analytics dashboard origin.`
310
+ );
311
+ }
312
+ let parsed = {};
313
+ try {
314
+ parsed = JSON.parse(text);
315
+ } catch {
316
+ parsed = { error: text };
317
+ }
318
+ throw new Error(
319
+ [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
320
+ );
321
+ }
322
+ try {
323
+ return JSON.parse(text);
324
+ } catch {
325
+ throw new Error(`JMP content HTTP ${res.status} \u2014 ${url} returned invalid JSON.`);
326
+ }
327
+ }
328
+ var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
329
+ function readProcessEnv(key) {
330
+ if (typeof process === "undefined" || !process.env) return void 0;
331
+ const v = process.env[key];
332
+ return typeof v === "string" && v.length > 0 ? v : void 0;
333
+ }
334
+ function getJmpBrowserAnalyticsConfigFromEnv(overrides) {
335
+ const baseUrl = readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
336
+ const trackingKey = readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
337
+ if (!baseUrl || !trackingKey) return null;
338
+ return {
339
+ baseUrl,
340
+ trackingKey,
341
+ pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
342
+ ...overrides
343
+ };
344
+ }
345
+ function getJmpServerContentConfigFromEnv(overrides) {
346
+ const baseUrl = readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
347
+ const trackingKey = readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
348
+ if (!baseUrl || !trackingKey) return null;
349
+ return { baseUrl, trackingKey, ...overrides };
350
+ }
351
+ function isJmpBrowserAnalyticsEnvConfigured() {
352
+ return Boolean(
353
+ readProcessEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL") && readProcessEnv("NEXT_PUBLIC_JMP_TRACKING_KEY")
354
+ );
355
+ }
356
+ function isJmpServerContentEnvConfigured() {
357
+ return isJmpBrowserAnalyticsEnvConfigured();
358
+ }
359
+ var jmpAnalyticsBrowserSingleton = null;
360
+ function getOrCreateJmpAnalyticsFromBrowserEnv(overrides) {
361
+ if (typeof window === "undefined") return null;
362
+ if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton;
363
+ const config = getJmpBrowserAnalyticsConfigFromEnv(overrides);
364
+ if (!config) return null;
365
+ jmpAnalyticsBrowserSingleton = createJmpAnalytics(config);
366
+ return jmpAnalyticsBrowserSingleton;
367
+ }
368
+ function createJmpContentClientFromServerEnv(overrides) {
369
+ const config = getJmpServerContentConfigFromEnv(overrides);
370
+ if (!config) return null;
371
+ return createJmpContentClient(config);
372
+ }
373
+
374
+ export {
375
+ createJmpAnalytics,
376
+ getJmpBlogPosts,
377
+ getJmpBlogPost,
378
+ getJmpFaqs,
379
+ createJmpContentClient,
380
+ DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
381
+ getJmpBrowserAnalyticsConfigFromEnv,
382
+ getJmpServerContentConfigFromEnv,
383
+ isJmpBrowserAnalyticsEnvConfigured,
384
+ isJmpServerContentEnvConfigured,
385
+ getOrCreateJmpAnalyticsFromBrowserEnv,
386
+ createJmpContentClientFromServerEnv
387
+ };
388
+ //# sourceMappingURL=chunk-RYFQXTZ7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type JmpEventType = \"page_view\" | \"form_submit\" | \"call_click\"\n\nexport type JmpAnalyticsConfig = {\n /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */\n baseUrl: string\n /** Per-website key from JMP admin */\n trackingKey: string\n /** Log requests to console */\n debug?: boolean\n /** Override fetch (tests, React Native) */\n fetchImpl?: typeof fetch\n /**\n * Suppress duplicate `page_view` for the same path within this window (ms).\n * Helps with React Strict Mode double-mount; typical: 500–1000.\n */\n pageViewDedupeMs?: number\n}\n\nexport type JmpContentConfig = Pick<\n JmpAnalyticsConfig,\n \"baseUrl\" | \"trackingKey\" | \"debug\" | \"fetchImpl\"\n>\n\nexport type JmpBlogPost = {\n id: string\n title: string\n slug: string\n excerpt: string | null\n body: string\n seo_title: string | null\n meta_description: string | null\n featured_image_url: string | null\n published_at: string | null\n updated_at: string\n}\n\nexport type JmpFaqItem = {\n id: string\n slug: string\n question: string\n answer: string\n sort_order: number\n updated_at: string\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 extras for contact forms (kept in `leads.metadata`), e.g.:\n * - preferredContact: \"email\" | \"phone\"\n * - utm_source / utm_medium / utm_campaign / utm_content / utm_term\n */\n metadata?: Record<string, unknown>\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\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 inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function 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 function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\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 try {\n return JSON.parse(text) as T\n } catch {\n throw new Error(`JMP content HTTP ${res.status} — ${url} returned invalid JSON.`)\n }\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":[]}