@jmp-technologies/analytics 0.1.6 → 0.1.8
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 +108 -5
- package/dist/{chunk-O6K2SIUM.js → chunk-JX54VUZ7.js} +2 -2
- package/dist/{chunk-RYFQXTZ7.js → chunk-XMVG3YES.js} +94 -7
- package/dist/chunk-XMVG3YES.js.map +1 -0
- package/dist/index.cjs +98 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -5
- package/dist/index.d.ts +77 -5
- package/dist/index.js +9 -3
- package/dist/next.cjs.map +1 -1
- package/dist/next.js +2 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-RYFQXTZ7.js.map +0 -1
- /package/dist/{chunk-O6K2SIUM.js.map → chunk-JX54VUZ7.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# `@jmp-technologies/analytics`
|
|
2
2
|
|
|
3
|
-
Browser client for **`POST {baseUrl}/api/events`** plus published blog
|
|
4
|
-
content helpers for client websites.
|
|
3
|
+
Browser client for **`POST {baseUrl}/api/events`** plus published blog, FAQ, and
|
|
4
|
+
**site contact profile** (`GET /api/content/site`) helpers for client websites.
|
|
5
5
|
|
|
6
6
|
## New client website (full process)
|
|
7
7
|
|
|
@@ -63,6 +63,56 @@ import { JmpNextAnalytics, JmpTrackedTelLink } from "@jmp-technologies/analytics
|
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
+
### Optional server-side page views for crawler visibility
|
|
67
|
+
|
|
68
|
+
Browser analytics may miss bots/crawlers that do not execute JavaScript. To capture AI crawler user agents more reliably, forward server requests from Next.js `proxy.ts` or route code:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
getJmpServerContentConfigFromEnv,
|
|
73
|
+
trackJmpServerPageView,
|
|
74
|
+
} from "@jmp-technologies/analytics"
|
|
75
|
+
|
|
76
|
+
export async function proxy(request: Request) {
|
|
77
|
+
const config = getJmpServerContentConfigFromEnv()
|
|
78
|
+
if (config) {
|
|
79
|
+
void trackJmpServerPageView(config, { request }).catch(() => {})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use this only on page routes you want counted. Avoid static assets, health checks, API routes, and duplicate tracking if a page already has browser tracking and you do not want both server and browser page views.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Optional AI crawler robots.txt policy
|
|
89
|
+
|
|
90
|
+
Clients can switch AI crawler blocking on or off in JMP dashboard settings. To make that setting visible to crawlers, serve the generated policy from the client site's `robots.txt` route:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import {
|
|
94
|
+
getJmpRobotsTxt,
|
|
95
|
+
getJmpServerContentConfigFromEnv,
|
|
96
|
+
} from "@jmp-technologies/analytics"
|
|
97
|
+
|
|
98
|
+
export async function GET() {
|
|
99
|
+
const config = getJmpServerContentConfigFromEnv()
|
|
100
|
+
const body = config
|
|
101
|
+
? await getJmpRobotsTxt(config)
|
|
102
|
+
: "# JMP Analytics is not configured.\n"
|
|
103
|
+
|
|
104
|
+
return new Response(body, {
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Place that in `app/robots.txt/route.ts` for Next.js App Router sites. The policy uses robots.txt `Disallow` rules for known AI crawler user agents; crawlers that ignore robots.txt or use undisclosed names may still access public pages.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
66
116
|
### Manual browser (any framework)
|
|
67
117
|
|
|
68
118
|
**Browser (client components)** — one module-level singleton per tab, default `pageViewDedupeMs` (800):
|
|
@@ -87,12 +137,47 @@ export default async function BlogPage() {
|
|
|
87
137
|
}
|
|
88
138
|
```
|
|
89
139
|
|
|
90
|
-
Or **`createJmpContentClientFromServerEnv()`** for `getBlogPosts()` / `getBlogPost()` / `getFaqs()` in one object.
|
|
140
|
+
Or **`createJmpContentClientFromServerEnv()`** for `getBlogPosts()` / `getBlogPost()` / `getFaqs()` / `getSiteProfile()` in one object.
|
|
91
141
|
|
|
92
142
|
**Configured?** `isJmpBrowserAnalyticsEnvConfigured()` and `isJmpServerContentEnvConfigured()`.
|
|
93
143
|
|
|
94
144
|
---
|
|
95
145
|
|
|
146
|
+
## Site contact profile (footer / contact page)
|
|
147
|
+
|
|
148
|
+
Manage contact details in the JMP dashboard at **`/content?tab=contact`**. The live site reads them from **`GET {baseUrl}/api/content/site`** (same tracking key + CORS as blog/FAQ).
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import {
|
|
152
|
+
getJmpSiteProfile,
|
|
153
|
+
getJmpServerContentConfigFromEnv,
|
|
154
|
+
} from "@jmp-technologies/analytics"
|
|
155
|
+
|
|
156
|
+
export default async function Footer() {
|
|
157
|
+
const config = getJmpServerContentConfigFromEnv()
|
|
158
|
+
if (!config) return null
|
|
159
|
+
const site = await getJmpSiteProfile(config)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<footer>
|
|
163
|
+
{site.phone ? <a href={`tel:${site.phone}`}>{site.phone}</a> : null}
|
|
164
|
+
{site.email ? <a href={`mailto:${site.email}`}>{site.email}</a> : null}
|
|
165
|
+
{site.cta.label && site.cta.url ? (
|
|
166
|
+
<a href={site.cta.url}>{site.cta.label}</a>
|
|
167
|
+
) : null}
|
|
168
|
+
{/* Developer-defined keys from dashboard (admin configures keys, client fills values): */}
|
|
169
|
+
{site.extras["license-number"] ? (
|
|
170
|
+
<p>License: {site.extras["license-number"]}</p>
|
|
171
|
+
) : null}
|
|
172
|
+
</footer>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`JmpSiteProfile` includes `name`, `display_name`, `email`, `phone`, `address`, `hours`, `service_area`, `google_maps_url`, `social` (linkedin, instagram, …), `cta`, `legal`, and **`extras`** (record of string keys → values).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
96
181
|
## Blog and FAQ content
|
|
97
182
|
|
|
98
183
|
Manage content in the JMP dashboard at `/content`, then render it on the
|
|
@@ -124,6 +209,7 @@ Available helpers:
|
|
|
124
209
|
|
|
125
210
|
FAQ items include a stable `slug` field. Use that slug, rather than the `id`,
|
|
126
211
|
when building FAQ URLs so tracked page views are stored as readable paths.
|
|
212
|
+
When present, `category` is an optional section label you can use to group FAQs in your UI.
|
|
127
213
|
|
|
128
214
|
**Config from env**
|
|
129
215
|
|
|
@@ -146,7 +232,9 @@ when building FAQ URLs so tracked page views are stored as readable paths.
|
|
|
146
232
|
- `getJmpBlogPosts(config)`
|
|
147
233
|
- `getJmpBlogPost(config, slug)`
|
|
148
234
|
- `getJmpFaqs(config)`
|
|
149
|
-
- `
|
|
235
|
+
- `getJmpSiteProfile(config)` — contact + `extras` (since **0.1.8**)
|
|
236
|
+
- `getJmpRobotsTxt(config)`
|
|
237
|
+
- `createJmpContentClient(config)` — includes `getSiteProfile()`
|
|
150
238
|
|
|
151
239
|
For SEO, call these helpers from server-rendered pages whenever the client site
|
|
152
240
|
framework supports it.
|
|
@@ -157,4 +245,19 @@ framework supports it.
|
|
|
157
245
|
|
|
158
246
|
## Publish to npm (org `jmp-technologies`)
|
|
159
247
|
|
|
160
|
-
|
|
248
|
+
From this directory:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npm run build
|
|
252
|
+
npm publish
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Package sets `publishConfig.access` to `public`. Requires `npm login` with publish access to `@jmp-technologies`.
|
|
256
|
+
|
|
257
|
+
After publish, on client sites:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm install @jmp-technologies/analytics@0.1.8
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Ensure the **dashboard** is deployed with migrations **020** and **021** and `/api/content/site` is live before relying on `getJmpSiteProfile` in production.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getOrCreateJmpAnalyticsFromBrowserEnv
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XMVG3YES.js";
|
|
4
4
|
|
|
5
5
|
// src/react.tsx
|
|
6
6
|
import { useMemo } from "react";
|
|
@@ -32,4 +32,4 @@ export {
|
|
|
32
32
|
useJmpAnalytics,
|
|
33
33
|
JmpTrackedTelLink
|
|
34
34
|
};
|
|
35
|
-
//# sourceMappingURL=chunk-
|
|
35
|
+
//# sourceMappingURL=chunk-JX54VUZ7.js.map
|
|
@@ -8,6 +8,31 @@ function responseLooksLikeHtml(body) {
|
|
|
8
8
|
function normalizeBaseUrl(baseUrl) {
|
|
9
9
|
return baseUrl.replace(/\/$/, "");
|
|
10
10
|
}
|
|
11
|
+
function getHeader(headers, key) {
|
|
12
|
+
if (!headers) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof headers.get === "function") {
|
|
16
|
+
return headers.get(key);
|
|
17
|
+
}
|
|
18
|
+
const record = headers;
|
|
19
|
+
const direct = record[key] ?? record[key.toLowerCase()];
|
|
20
|
+
if (Array.isArray(direct)) {
|
|
21
|
+
return direct[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
return direct ?? null;
|
|
24
|
+
}
|
|
25
|
+
function pathFromRequestUrl(url) {
|
|
26
|
+
if (!url) {
|
|
27
|
+
return "/";
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const u = new URL(url);
|
|
31
|
+
return `${u.pathname}${u.search}`;
|
|
32
|
+
} catch {
|
|
33
|
+
return url.startsWith("/") ? url : "/";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
11
36
|
function inferDevice() {
|
|
12
37
|
if (typeof window === "undefined") {
|
|
13
38
|
return "unknown";
|
|
@@ -251,6 +276,47 @@ function createJmpAnalytics(config) {
|
|
|
251
276
|
subscribeToSpaNavigation
|
|
252
277
|
};
|
|
253
278
|
}
|
|
279
|
+
async function trackJmpServerPageView(config, options = {}) {
|
|
280
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
281
|
+
const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
|
|
282
|
+
if (!resolvedFetch) {
|
|
283
|
+
throw new Error("JMP server analytics: fetch is not available; pass fetchImpl in config");
|
|
284
|
+
}
|
|
285
|
+
const visitorUserAgent = options.userAgent ?? getHeader(options.request?.headers, "user-agent");
|
|
286
|
+
const visitorReferer = options.referer ?? getHeader(options.request?.headers, "referer");
|
|
287
|
+
const path = options.path ?? pathFromRequestUrl(options.request?.url);
|
|
288
|
+
const headers = {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
"X-JMP-Tracking-Key": config.trackingKey
|
|
291
|
+
};
|
|
292
|
+
if (visitorUserAgent) {
|
|
293
|
+
headers["X-JMP-Visitor-User-Agent"] = visitorUserAgent;
|
|
294
|
+
}
|
|
295
|
+
if (visitorReferer) {
|
|
296
|
+
headers["X-JMP-Visitor-Referer"] = visitorReferer;
|
|
297
|
+
}
|
|
298
|
+
const payload = {
|
|
299
|
+
type: "page_view",
|
|
300
|
+
path: path || "/",
|
|
301
|
+
metadata: options.metadata && typeof options.metadata === "object" ? options.metadata : {}
|
|
302
|
+
};
|
|
303
|
+
if (options.sessionId) {
|
|
304
|
+
payload.sessionId = options.sessionId;
|
|
305
|
+
}
|
|
306
|
+
const url = `${baseUrl}/api/events`;
|
|
307
|
+
if (config.debug) {
|
|
308
|
+
console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
|
|
309
|
+
}
|
|
310
|
+
const res = await resolvedFetch(url, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers,
|
|
313
|
+
body: JSON.stringify(payload)
|
|
314
|
+
});
|
|
315
|
+
if (!res.ok) {
|
|
316
|
+
const text = await res.text();
|
|
317
|
+
throw new Error(`JMP server analytics HTTP ${res.status} \u2014 ${text}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
254
320
|
async function getJmpBlogPosts(config) {
|
|
255
321
|
const data = await getContent(
|
|
256
322
|
config,
|
|
@@ -279,14 +345,36 @@ async function getJmpFaqs(config) {
|
|
|
279
345
|
);
|
|
280
346
|
return data.faqs;
|
|
281
347
|
}
|
|
348
|
+
async function getJmpSiteProfile(config) {
|
|
349
|
+
const data = await getContent(
|
|
350
|
+
config,
|
|
351
|
+
"/api/content/site"
|
|
352
|
+
);
|
|
353
|
+
return data.site;
|
|
354
|
+
}
|
|
355
|
+
async function getJmpRobotsTxt(config) {
|
|
356
|
+
return getContentText(config, "/api/content/robots");
|
|
357
|
+
}
|
|
282
358
|
function createJmpContentClient(config) {
|
|
283
359
|
return {
|
|
284
360
|
getBlogPosts: () => getJmpBlogPosts(config),
|
|
285
361
|
getBlogPost: (slug) => getJmpBlogPost(config, slug),
|
|
286
|
-
getFaqs: () => getJmpFaqs(config)
|
|
362
|
+
getFaqs: () => getJmpFaqs(config),
|
|
363
|
+
getSiteProfile: () => getJmpSiteProfile(config),
|
|
364
|
+
getRobotsTxt: () => getJmpRobotsTxt(config)
|
|
287
365
|
};
|
|
288
366
|
}
|
|
289
367
|
async function getContent(config, path) {
|
|
368
|
+
const text = await getContentText(config, path);
|
|
369
|
+
try {
|
|
370
|
+
return JSON.parse(text);
|
|
371
|
+
} catch {
|
|
372
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
373
|
+
const url = `${baseUrl}${path}`;
|
|
374
|
+
throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function getContentText(config, path) {
|
|
290
378
|
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
291
379
|
const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
|
|
292
380
|
if (!resolvedFetch) {
|
|
@@ -319,11 +407,7 @@ async function getContent(config, path) {
|
|
|
319
407
|
[`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
|
|
320
408
|
);
|
|
321
409
|
}
|
|
322
|
-
|
|
323
|
-
return JSON.parse(text);
|
|
324
|
-
} catch {
|
|
325
|
-
throw new Error(`JMP content HTTP ${res.status} \u2014 ${url} returned invalid JSON.`);
|
|
326
|
-
}
|
|
410
|
+
return text;
|
|
327
411
|
}
|
|
328
412
|
var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
|
|
329
413
|
function readProcessEnv(key) {
|
|
@@ -373,9 +457,12 @@ function createJmpContentClientFromServerEnv(overrides) {
|
|
|
373
457
|
|
|
374
458
|
export {
|
|
375
459
|
createJmpAnalytics,
|
|
460
|
+
trackJmpServerPageView,
|
|
376
461
|
getJmpBlogPosts,
|
|
377
462
|
getJmpBlogPost,
|
|
378
463
|
getJmpFaqs,
|
|
464
|
+
getJmpSiteProfile,
|
|
465
|
+
getJmpRobotsTxt,
|
|
379
466
|
createJmpContentClient,
|
|
380
467
|
DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
|
|
381
468
|
getJmpBrowserAnalyticsConfigFromEnv,
|
|
@@ -385,4 +472,4 @@ export {
|
|
|
385
472
|
getOrCreateJmpAnalyticsFromBrowserEnv,
|
|
386
473
|
createJmpContentClientFromServerEnv
|
|
387
474
|
};
|
|
388
|
-
//# sourceMappingURL=chunk-
|
|
475
|
+
//# sourceMappingURL=chunk-XMVG3YES.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 category?: string | null\n sort_order: number\n updated_at: string\n}\n\nexport type JmpSiteProfile = {\n name: string\n display_name: string | null\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n service_area: string | null\n google_maps_url: string | null\n social: {\n linkedin: string | null\n instagram: string | null\n facebook: string | null\n x: string | null\n youtube: string | null\n }\n cta: {\n label: string | null\n url: string | null\n }\n legal: {\n privacy_policy_url: string | null\n terms_url: string | null\n }\n /** Developer-defined keys; values filled by the client. */\n extras: Record<string, string | null>\n}\n\nexport type TrackPageViewOptions = {\n path?: string\n source?: string\n device?: string\n country?: string\n metadata?: Record<string, unknown>\n}\n\nexport type TrackLeadOptions = {\n /** Path where the lead was submitted, e.g. /contact */\n path?: string\n name?: string\n email?: string\n phone?: string\n company?: string\n website?: string\n subject?: string\n message?: string\n /**\n * Optional flexible metadata for dynamic lead forms.\n * Prefer the normalized envelope (formId/formVersion/answers/derived/attribution),\n * but unknown keys are also accepted by the API.\n */\n metadata?: LeadMetadata\n /**\n * Optional session id (from createJmpAnalytics internal session) if your app wants to pass it explicitly.\n * Most callers should omit this.\n */\n sessionId?: string\n}\n\nexport type LeadMetadataValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | LeadMetadataValue[]\n | { [key: string]: LeadMetadataValue }\n\nexport type LeadMetadata = {\n formId?: string\n formVersion?: string\n leadKind?: string\n answers?: Record<string, LeadMetadataValue>\n derived?: Record<string, LeadMetadataValue>\n attribution?: {\n utm_source?: string\n utm_medium?: string\n utm_campaign?: string\n utm_content?: string\n utm_term?: string\n gclid?: string\n fbclid?: string\n msclkid?: string\n ttclid?: string\n [key: string]: LeadMetadataValue | undefined\n }\n tags?: string[]\n [key: string]: LeadMetadataValue | undefined\n}\n\nexport type JmpServerRequestLike = {\n url?: string\n headers:\n | Pick<Headers, \"get\">\n | Record<string, string | string[] | undefined | null>\n}\n\nexport type TrackServerPageViewOptions = {\n request?: JmpServerRequestLike\n /** Overrides request URL path. Use pathname + search when possible. */\n path?: string\n /** Original visitor user agent. Useful when not passing `request`. */\n userAgent?: string | null\n /** Original visitor referrer. Useful when not passing `request`. */\n referer?: string | null\n metadata?: Record<string, unknown>\n sessionId?: string\n}\n\nconst DEFAULT_SOURCE = \"direct\"\nconst SESSION_STORAGE_KEY = \"jmp_analytics_sid\"\n\n/** CDN / Next.js often return HTML error pages; API errors are JSON */\nfunction responseLooksLikeHtml(body: string) {\n const s = body.trimStart()\n return (\n s.startsWith(\"<!\") ||\n s.toLowerCase().startsWith(\"<html\") ||\n (s.startsWith(\"<\") && /<html[\\s>]/i.test(s.slice(0, 500)))\n )\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\")\n}\n\nfunction getHeader(\n headers: JmpServerRequestLike[\"headers\"] | undefined,\n key: string\n): string | null {\n if (!headers) {\n return null\n }\n if (typeof (headers as Pick<Headers, \"get\">).get === \"function\") {\n return (headers as Pick<Headers, \"get\">).get(key)\n }\n const record = headers as Record<string, string | string[] | undefined | null>\n const direct = record[key] ?? record[key.toLowerCase()]\n if (Array.isArray(direct)) {\n return direct[0] ?? null\n }\n return direct ?? null\n}\n\nfunction pathFromRequestUrl(url: string | undefined): string {\n if (!url) {\n return \"/\"\n }\n try {\n const u = new URL(url)\n return `${u.pathname}${u.search}`\n } catch {\n return url.startsWith(\"/\") ? url : \"/\"\n }\n}\n\nfunction inferDevice(): string {\n if (typeof window === \"undefined\") {\n return \"unknown\"\n }\n return window.innerWidth < 768 ? \"mobile\" : \"desktop\"\n}\n\n/** Stable per-tab session id for bounce and unique session metrics (server-stored). */\nfunction getOrCreateSessionId(): string | undefined {\n if (typeof window === \"undefined\" || !globalThis.crypto?.randomUUID) {\n return undefined\n }\n try {\n let id = sessionStorage.getItem(SESSION_STORAGE_KEY)\n if (!id) {\n id = crypto.randomUUID()\n sessionStorage.setItem(SESSION_STORAGE_KEY, id)\n }\n return id\n } catch {\n return undefined\n }\n}\n\n/**\n * Create a tracker instance. Safe to call once per app and reuse.\n */\nexport function createJmpAnalytics(config: JmpAnalyticsConfig) {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\n \"createJmpAnalytics: fetch is not available; pass fetchImpl in config\"\n )\n }\n\n const fetchFn: typeof fetch = resolvedFetch\n\n let lastDedupePath = \"\"\n let lastDedupeAt = 0\n\n async function send(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/events`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/events), ` +\n `not a marketing or client site unless that codebase ships the same API route.`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n async function sendLead(body: Record<string, unknown>) {\n const url = `${baseUrl}/api/leads`\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n\n const sid = getOrCreateSessionId()\n const payload = { ...body, ...(sid ? { sessionId: sid } : {}) }\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await fetchFn(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n keepalive: true,\n })\n\n if (!res.ok) {\n const text = await res.text()\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP Analytics HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl / NEXT_PUBLIC_JMP_ANALYTICS_URL to your JMP Analytics dashboard origin (the deployment that includes POST /api/leads).`\n )\n }\n let parsed: {\n error?: string\n code?: string\n hint?: string\n expectedDomain?: string\n } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n const detail = parsed.error ?? text\n const segments = [\n `JMP Analytics HTTP ${res.status}`,\n parsed.code ? `[${parsed.code}]` : null,\n detail,\n parsed.hint ? `Hint: ${parsed.hint}` : null,\n parsed.expectedDomain\n ? `Domain in JMP admin: ${parsed.expectedDomain}`\n : null,\n ].filter(Boolean) as string[]\n const message = segments.join(\" — \")\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"error response\", {\n status: res.status,\n ...parsed,\n raw: text,\n })\n }\n throw new Error(message)\n }\n }\n\n function shouldDedupePageView(path: string): boolean {\n const ms = config.pageViewDedupeMs ?? 0\n if (ms <= 0 || typeof window === \"undefined\") {\n return false\n }\n const now = Date.now()\n if (path === lastDedupePath && now - lastDedupeAt < ms) {\n return true\n }\n lastDedupePath = path\n lastDedupeAt = now\n return false\n }\n\n function trackPageView(options?: TrackPageViewOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n if (shouldDedupePageView(path)) {\n return Promise.resolve()\n }\n\n return send({\n type: \"page_view\",\n path: path || \"/\",\n source: options?.source ?? DEFAULT_SOURCE,\n device: options?.device ?? inferDevice(),\n country: options?.country,\n metadata:\n options?.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n })\n }\n\n function trackFormSubmit(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"form_submit\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackCallClick(\n path: string,\n metadata?: Record<string, unknown>\n ) {\n return send({\n type: \"call_click\",\n path: path || \"/\",\n metadata: metadata ?? {},\n })\n }\n\n function trackLead(options?: TrackLeadOptions) {\n const path =\n options?.path ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const body: Record<string, unknown> = { path: path || \"/\" }\n if (options?.name) body.name = options.name\n if (options?.email) body.email = options.email\n if (options?.phone) body.phone = options.phone\n if (options?.company) body.company = options.company\n if (options?.website) body.website = options.website\n if (options?.subject) body.subject = options.subject\n if (options?.message) body.message = options.message\n if (options?.metadata && typeof options.metadata === \"object\") {\n body.metadata = options.metadata\n }\n if (options?.sessionId) body.sessionId = options.sessionId\n\n return sendLead(body)\n }\n\n function track(\n type: JmpEventType,\n payload: {\n path: string\n metadata?: Record<string, unknown>\n source?: string\n device?: string\n country?: string\n }\n ) {\n if (type === \"page_view\") {\n return trackPageView({\n path: payload.path,\n source: payload.source,\n device: payload.device,\n country: payload.country,\n metadata: payload.metadata,\n })\n }\n if (type === \"form_submit\") {\n return trackFormSubmit(payload.path, payload.metadata)\n }\n return trackCallClick(payload.path, payload.metadata)\n }\n\n /**\n * Fire `page_view` on browser back/forward and when `history.pushState` /\n * `replaceState` are used (many SPAs). Call once after creating the tracker.\n * Returns an unsubscribe function.\n */\n function subscribeToSpaNavigation(): () => void {\n if (typeof window === \"undefined\") {\n return () => {}\n }\n\n let lastPath = window.location.pathname\n\n const run = () => {\n const path = window.location.pathname\n if (path !== lastPath) {\n lastPath = path\n void trackPageView({ path })\n }\n }\n\n window.addEventListener(\"popstate\", run)\n\n const push = history.pushState.bind(history)\n const replace = history.replaceState.bind(history)\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n const r = push(...args)\n queueMicrotask(run)\n return r\n }\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n const r = replace(...args)\n queueMicrotask(run)\n return r\n }\n\n return () => {\n window.removeEventListener(\"popstate\", run)\n history.pushState = push\n history.replaceState = replace\n }\n }\n\n return {\n track,\n trackPageView,\n trackFormSubmit,\n trackCallClick,\n trackLead,\n subscribeToSpaNavigation,\n }\n}\n\nexport type JmpAnalytics = ReturnType<typeof createJmpAnalytics>\n\nexport async function trackJmpServerPageView(\n config: JmpContentConfig,\n options: TrackServerPageViewOptions = {}\n): Promise<void> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP server analytics: fetch is not available; pass fetchImpl in config\")\n }\n\n const visitorUserAgent =\n options.userAgent ?? getHeader(options.request?.headers, \"user-agent\")\n const visitorReferer =\n options.referer ?? getHeader(options.request?.headers, \"referer\")\n const path = options.path ?? pathFromRequestUrl(options.request?.url)\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-JMP-Tracking-Key\": config.trackingKey,\n }\n if (visitorUserAgent) {\n headers[\"X-JMP-Visitor-User-Agent\"] = visitorUserAgent\n }\n if (visitorReferer) {\n headers[\"X-JMP-Visitor-Referer\"] = visitorReferer\n }\n\n const payload: Record<string, unknown> = {\n type: \"page_view\",\n path: path || \"/\",\n metadata:\n options.metadata && typeof options.metadata === \"object\"\n ? options.metadata\n : {},\n }\n if (options.sessionId) {\n payload.sessionId = options.sessionId\n }\n\n const url = `${baseUrl}/api/events`\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"POST\", url, payload)\n }\n\n const res = await resolvedFetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`JMP server analytics HTTP ${res.status} — ${text}`)\n }\n}\n\nexport async function getJmpBlogPosts(\n config: JmpContentConfig\n): Promise<JmpBlogPost[]> {\n const data = await getContent<{ posts: JmpBlogPost[] }>(\n config,\n \"/api/content/blog\"\n )\n return data.posts\n}\n\nexport async function getJmpBlogPost(\n config: JmpContentConfig,\n slug: string\n): Promise<JmpBlogPost | null> {\n try {\n const data = await getContent<{ post: JmpBlogPost }>(\n config,\n `/api/content/blog/${encodeURIComponent(slug)}`\n )\n return data.post\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"HTTP 404\")) {\n return null\n }\n throw error\n }\n}\n\nexport async function getJmpFaqs(\n config: JmpContentConfig\n): Promise<JmpFaqItem[]> {\n const data = await getContent<{ faqs: JmpFaqItem[] }>(\n config,\n \"/api/content/faqs\"\n )\n return data.faqs\n}\n\nexport async function getJmpSiteProfile(\n config: JmpContentConfig\n): Promise<JmpSiteProfile> {\n const data = await getContent<{ site: JmpSiteProfile }>(\n config,\n \"/api/content/site\"\n )\n return data.site\n}\n\nexport async function getJmpRobotsTxt(config: JmpContentConfig): Promise<string> {\n return getContentText(config, \"/api/content/robots\")\n}\n\nexport function createJmpContentClient(config: JmpContentConfig) {\n return {\n getBlogPosts: () => getJmpBlogPosts(config),\n getBlogPost: (slug: string) => getJmpBlogPost(config, slug),\n getFaqs: () => getJmpFaqs(config),\n getSiteProfile: () => getJmpSiteProfile(config),\n getRobotsTxt: () => getJmpRobotsTxt(config),\n }\n}\n\nasync function getContent<T>(\n config: JmpContentConfig,\n path: string\n): Promise<T> {\n const text = await getContentText(config, path)\n\n try {\n return JSON.parse(text) as T\n } catch {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const url = `${baseUrl}${path}`\n throw new Error(`JMP content HTTP 200 — ${url} returned invalid JSON.`)\n }\n}\n\nasync function getContentText(\n config: JmpContentConfig,\n path: string\n): Promise<string> {\n const baseUrl = normalizeBaseUrl(config.baseUrl)\n const resolvedFetch =\n config.fetchImpl ??\n (typeof globalThis !== \"undefined\" && globalThis.fetch\n ? globalThis.fetch.bind(globalThis)\n : undefined)\n\n if (!resolvedFetch) {\n throw new Error(\"JMP content: fetch is not available; pass fetchImpl in config\")\n }\n\n const url = `${baseUrl}${path}`\n\n if (config.debug) {\n console.debug(\"[@jmp-technologies/analytics]\", \"GET\", url)\n }\n\n const res = await resolvedFetch(url, {\n method: \"GET\",\n headers: {\n \"X-JMP-Tracking-Key\": config.trackingKey,\n },\n })\n\n const text = await res.text()\n\n if (!res.ok) {\n if (responseLooksLikeHtml(text)) {\n throw new Error(\n `JMP content HTTP ${res.status} — ${url} returned HTML (not the JSON API). ` +\n `Set baseUrl to your JMP Analytics dashboard origin.`\n )\n }\n\n let parsed: { error?: string; hint?: string } = {}\n try {\n parsed = JSON.parse(text) as typeof parsed\n } catch {\n parsed = { error: text }\n }\n\n throw new Error(\n [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint]\n .filter(Boolean)\n .join(\" — \")\n )\n }\n\n return text\n}\n\n/** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */\nexport const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800\n\nfunction readProcessEnv(key: string): string | undefined {\n if (typeof process === \"undefined\" || !process.env) return undefined\n const v = process.env[key]\n return typeof v === \"string\" && v.length > 0 ? v : undefined\n}\n\n/**\n * Browser / client bundle: reads **only** `NEXT_PUBLIC_*` vars.\n * Use for `createJmpAnalytics` on the client. Returns `null` if incomplete.\n */\nexport function getJmpBrowserAnalyticsConfigFromEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalyticsConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return {\n baseUrl,\n trackingKey,\n pageViewDedupeMs: DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,\n ...overrides,\n }\n}\n\n/**\n * Server / Node (SSR, Route Handlers, `getStaticProps`, etc.):\n * Same **`NEXT_PUBLIC_*`** vars as the browser (`NEXT_PUBLIC_JMP_ANALYTICS_URL` +\n * `NEXT_PUBLIC_JMP_TRACKING_KEY`). Next.js injects these on the server for RSC/fetches.\n * Returns `null` if incomplete.\n */\nexport function getJmpServerContentConfigFromEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentConfig | null {\n const baseUrl = readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\")\n const trackingKey = readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n if (!baseUrl || !trackingKey) return null\n return { baseUrl, trackingKey, ...overrides }\n}\n\nexport function isJmpBrowserAnalyticsEnvConfigured(): boolean {\n return Boolean(\n readProcessEnv(\"NEXT_PUBLIC_JMP_ANALYTICS_URL\") &&\n readProcessEnv(\"NEXT_PUBLIC_JMP_TRACKING_KEY\")\n )\n}\n\nexport function isJmpServerContentEnvConfigured(): boolean {\n return isJmpBrowserAnalyticsEnvConfigured()\n}\n\nlet jmpAnalyticsBrowserSingleton: JmpAnalytics | null = null\n\n/**\n * One `JmpAnalytics` instance per browser tab (module singleton). Safe for page views and events.\n * Returns `null` on the server or when public env vars are missing.\n */\nexport function getOrCreateJmpAnalyticsFromBrowserEnv(\n overrides?: Partial<JmpAnalyticsConfig>\n): JmpAnalytics | null {\n if (typeof window === \"undefined\") return null\n if (jmpAnalyticsBrowserSingleton) return jmpAnalyticsBrowserSingleton\n const config = getJmpBrowserAnalyticsConfigFromEnv(overrides)\n if (!config) return null\n jmpAnalyticsBrowserSingleton = createJmpAnalytics(config)\n return jmpAnalyticsBrowserSingleton\n}\n\nexport type JmpContentClient = ReturnType<typeof createJmpContentClient>\n\n/**\n * `createJmpContentClient` using server env resolution. Returns `null` if env is incomplete.\n */\nexport function createJmpContentClientFromServerEnv(\n overrides?: Partial<JmpContentConfig>\n): JmpContentClient | null {\n const config = getJmpServerContentConfigFromEnv(overrides)\n if (!config) return null\n return createJmpContentClient(config)\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -28,10 +28,13 @@ __export(index_exports, {
|
|
|
28
28
|
getJmpBlogPosts: () => getJmpBlogPosts,
|
|
29
29
|
getJmpBrowserAnalyticsConfigFromEnv: () => getJmpBrowserAnalyticsConfigFromEnv,
|
|
30
30
|
getJmpFaqs: () => getJmpFaqs,
|
|
31
|
+
getJmpRobotsTxt: () => getJmpRobotsTxt,
|
|
31
32
|
getJmpServerContentConfigFromEnv: () => getJmpServerContentConfigFromEnv,
|
|
33
|
+
getJmpSiteProfile: () => getJmpSiteProfile,
|
|
32
34
|
getOrCreateJmpAnalyticsFromBrowserEnv: () => getOrCreateJmpAnalyticsFromBrowserEnv,
|
|
33
35
|
isJmpBrowserAnalyticsEnvConfigured: () => isJmpBrowserAnalyticsEnvConfigured,
|
|
34
|
-
isJmpServerContentEnvConfigured: () => isJmpServerContentEnvConfigured
|
|
36
|
+
isJmpServerContentEnvConfigured: () => isJmpServerContentEnvConfigured,
|
|
37
|
+
trackJmpServerPageView: () => trackJmpServerPageView
|
|
35
38
|
});
|
|
36
39
|
module.exports = __toCommonJS(index_exports);
|
|
37
40
|
var DEFAULT_SOURCE = "direct";
|
|
@@ -43,6 +46,31 @@ function responseLooksLikeHtml(body) {
|
|
|
43
46
|
function normalizeBaseUrl(baseUrl) {
|
|
44
47
|
return baseUrl.replace(/\/$/, "");
|
|
45
48
|
}
|
|
49
|
+
function getHeader(headers, key) {
|
|
50
|
+
if (!headers) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (typeof headers.get === "function") {
|
|
54
|
+
return headers.get(key);
|
|
55
|
+
}
|
|
56
|
+
const record = headers;
|
|
57
|
+
const direct = record[key] ?? record[key.toLowerCase()];
|
|
58
|
+
if (Array.isArray(direct)) {
|
|
59
|
+
return direct[0] ?? null;
|
|
60
|
+
}
|
|
61
|
+
return direct ?? null;
|
|
62
|
+
}
|
|
63
|
+
function pathFromRequestUrl(url) {
|
|
64
|
+
if (!url) {
|
|
65
|
+
return "/";
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const u = new URL(url);
|
|
69
|
+
return `${u.pathname}${u.search}`;
|
|
70
|
+
} catch {
|
|
71
|
+
return url.startsWith("/") ? url : "/";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
46
74
|
function inferDevice() {
|
|
47
75
|
if (typeof window === "undefined") {
|
|
48
76
|
return "unknown";
|
|
@@ -286,6 +314,47 @@ function createJmpAnalytics(config) {
|
|
|
286
314
|
subscribeToSpaNavigation
|
|
287
315
|
};
|
|
288
316
|
}
|
|
317
|
+
async function trackJmpServerPageView(config, options = {}) {
|
|
318
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
319
|
+
const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
|
|
320
|
+
if (!resolvedFetch) {
|
|
321
|
+
throw new Error("JMP server analytics: fetch is not available; pass fetchImpl in config");
|
|
322
|
+
}
|
|
323
|
+
const visitorUserAgent = options.userAgent ?? getHeader(options.request?.headers, "user-agent");
|
|
324
|
+
const visitorReferer = options.referer ?? getHeader(options.request?.headers, "referer");
|
|
325
|
+
const path = options.path ?? pathFromRequestUrl(options.request?.url);
|
|
326
|
+
const headers = {
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
"X-JMP-Tracking-Key": config.trackingKey
|
|
329
|
+
};
|
|
330
|
+
if (visitorUserAgent) {
|
|
331
|
+
headers["X-JMP-Visitor-User-Agent"] = visitorUserAgent;
|
|
332
|
+
}
|
|
333
|
+
if (visitorReferer) {
|
|
334
|
+
headers["X-JMP-Visitor-Referer"] = visitorReferer;
|
|
335
|
+
}
|
|
336
|
+
const payload = {
|
|
337
|
+
type: "page_view",
|
|
338
|
+
path: path || "/",
|
|
339
|
+
metadata: options.metadata && typeof options.metadata === "object" ? options.metadata : {}
|
|
340
|
+
};
|
|
341
|
+
if (options.sessionId) {
|
|
342
|
+
payload.sessionId = options.sessionId;
|
|
343
|
+
}
|
|
344
|
+
const url = `${baseUrl}/api/events`;
|
|
345
|
+
if (config.debug) {
|
|
346
|
+
console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
|
|
347
|
+
}
|
|
348
|
+
const res = await resolvedFetch(url, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers,
|
|
351
|
+
body: JSON.stringify(payload)
|
|
352
|
+
});
|
|
353
|
+
if (!res.ok) {
|
|
354
|
+
const text = await res.text();
|
|
355
|
+
throw new Error(`JMP server analytics HTTP ${res.status} \u2014 ${text}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
289
358
|
async function getJmpBlogPosts(config) {
|
|
290
359
|
const data = await getContent(
|
|
291
360
|
config,
|
|
@@ -314,14 +383,36 @@ async function getJmpFaqs(config) {
|
|
|
314
383
|
);
|
|
315
384
|
return data.faqs;
|
|
316
385
|
}
|
|
386
|
+
async function getJmpSiteProfile(config) {
|
|
387
|
+
const data = await getContent(
|
|
388
|
+
config,
|
|
389
|
+
"/api/content/site"
|
|
390
|
+
);
|
|
391
|
+
return data.site;
|
|
392
|
+
}
|
|
393
|
+
async function getJmpRobotsTxt(config) {
|
|
394
|
+
return getContentText(config, "/api/content/robots");
|
|
395
|
+
}
|
|
317
396
|
function createJmpContentClient(config) {
|
|
318
397
|
return {
|
|
319
398
|
getBlogPosts: () => getJmpBlogPosts(config),
|
|
320
399
|
getBlogPost: (slug) => getJmpBlogPost(config, slug),
|
|
321
|
-
getFaqs: () => getJmpFaqs(config)
|
|
400
|
+
getFaqs: () => getJmpFaqs(config),
|
|
401
|
+
getSiteProfile: () => getJmpSiteProfile(config),
|
|
402
|
+
getRobotsTxt: () => getJmpRobotsTxt(config)
|
|
322
403
|
};
|
|
323
404
|
}
|
|
324
405
|
async function getContent(config, path) {
|
|
406
|
+
const text = await getContentText(config, path);
|
|
407
|
+
try {
|
|
408
|
+
return JSON.parse(text);
|
|
409
|
+
} catch {
|
|
410
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
411
|
+
const url = `${baseUrl}${path}`;
|
|
412
|
+
throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function getContentText(config, path) {
|
|
325
416
|
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
326
417
|
const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
|
|
327
418
|
if (!resolvedFetch) {
|
|
@@ -354,11 +445,7 @@ async function getContent(config, path) {
|
|
|
354
445
|
[`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
|
|
355
446
|
);
|
|
356
447
|
}
|
|
357
|
-
|
|
358
|
-
return JSON.parse(text);
|
|
359
|
-
} catch {
|
|
360
|
-
throw new Error(`JMP content HTTP ${res.status} \u2014 ${url} returned invalid JSON.`);
|
|
361
|
-
}
|
|
448
|
+
return text;
|
|
362
449
|
}
|
|
363
450
|
var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
|
|
364
451
|
function readProcessEnv(key) {
|
|
@@ -415,9 +502,12 @@ function createJmpContentClientFromServerEnv(overrides) {
|
|
|
415
502
|
getJmpBlogPosts,
|
|
416
503
|
getJmpBrowserAnalyticsConfigFromEnv,
|
|
417
504
|
getJmpFaqs,
|
|
505
|
+
getJmpRobotsTxt,
|
|
418
506
|
getJmpServerContentConfigFromEnv,
|
|
507
|
+
getJmpSiteProfile,
|
|
419
508
|
getOrCreateJmpAnalyticsFromBrowserEnv,
|
|
420
509
|
isJmpBrowserAnalyticsEnvConfigured,
|
|
421
|
-
isJmpServerContentEnvConfigured
|
|
510
|
+
isJmpServerContentEnvConfigured,
|
|
511
|
+
trackJmpServerPageView
|
|
422
512
|
});
|
|
423
513
|
//# sourceMappingURL=index.cjs.map
|