@jmp-technologies/analytics 0.1.11 → 0.1.14

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/index.d.cts CHANGED
@@ -1,3 +1,18 @@
1
+ type EarlyPageViewBeaconArgs = {
2
+ baseUrl: string;
3
+ trackingKey: string;
4
+ };
5
+ /**
6
+ * Capture landing referrer/UTMs and send an early page_view (fetch keepalive).
7
+ * Safe to call from inline script or module init.
8
+ */
9
+ declare function sendEarlyPageViewBeacon(args: EarlyPageViewBeaconArgs): boolean;
10
+ /** Inline script for Next.js `beforeInteractive` — earliest capture + beacon. */
11
+ declare function buildLandingCaptureInlineScript(args: {
12
+ baseUrl: string;
13
+ trackingKey: string;
14
+ }): string;
15
+
1
16
  type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click";
2
17
  type JmpAnalyticsConfig = {
3
18
  /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */
@@ -14,7 +29,22 @@ type JmpAnalyticsConfig = {
14
29
  */
15
30
  pageViewDedupeMs?: number;
16
31
  };
17
- type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "debug" | "fetchImpl">;
32
+ type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "debug" | "fetchImpl"> & {
33
+ /**
34
+ * Content wrappers wired in this site's template (blog, faq, trust.*, contact).
35
+ * Reported via POST /api/content/capabilities so unused dashboard tabs stay hidden.
36
+ * Call {@link registerJmpContentWrappers} at startup / deploy.
37
+ */
38
+ contentWrappers?: JmpContentWrapper[];
39
+ /**
40
+ * @deprecated Use `contentWrappers` (e.g. `trust.testimonials` instead of `testimonials`).
41
+ */
42
+ trustSlots?: JmpTrustSlot[];
43
+ };
44
+ /** Top-level and trust sub-wrappers the client can declare. */
45
+ type JmpContentWrapper = "blog" | "faq" | "contact" | "trust" | "trust.summary" | "trust.testimonials" | "trust.reviews" | "trust.stats" | "trust.logos" | "trust.credentials";
46
+ /** @deprecated Trust-only shorthand; prefer `JmpContentWrapper`. */
47
+ type JmpTrustSlot = "summary" | "testimonials" | "reviews" | "stats" | "logos" | "credentials";
18
48
  type JmpBlogPost = {
19
49
  id: string;
20
50
  title: string;
@@ -51,11 +81,17 @@ type JmpTestimonial = {
51
81
  author_name: string;
52
82
  author_descriptor: string | null;
53
83
  location: string | null;
84
+ /** 1–10 (half-star steps on a 5-star scale; 10 = 5.0 stars). */
54
85
  rating: number | null;
55
86
  image_url: string | null;
56
87
  video_url: string | null;
88
+ /** Optional topic or service line (e.g. roofing). */
57
89
  category: string | null;
90
+ /** Site section key — filter like FAQ `category` (e.g. testimonials, reviews). */
91
+ placement: string | null;
58
92
  source: string | null;
93
+ /** Optional link to the original review on Google, Yelp, etc. */
94
+ source_url: string | null;
59
95
  featured: boolean;
60
96
  sort_order: number;
61
97
  updated_at: string;
@@ -216,6 +252,25 @@ declare function getJmpFaqs(config: JmpContentConfig): Promise<JmpFaqItem[]>;
216
252
  declare function getJmpTrustBundle(config: JmpContentConfig): Promise<JmpTrustBundle>;
217
253
  declare function getJmpSiteProfile(config: JmpContentConfig): Promise<JmpSiteProfile>;
218
254
  declare function getJmpRobotsTxt(config: JmpContentConfig): Promise<string>;
255
+ /**
256
+ * Tell the JMP dashboard which content wrappers this site wires (blog, faq, trust.*, contact).
257
+ * Safe to call on every cold start; dedupes by tracking key + wrapper list.
258
+ */
259
+ declare function registerJmpContentWrappers(config: JmpContentConfig): Promise<{
260
+ ok: true;
261
+ wrappers: string[];
262
+ } | {
263
+ ok: false;
264
+ skipped: true;
265
+ }>;
266
+ /** @deprecated Use {@link registerJmpContentWrappers} with `contentWrappers`. */
267
+ declare function registerJmpTrustCapabilities(config: JmpContentConfig): Promise<{
268
+ ok: true;
269
+ slots: JmpTrustSlot[];
270
+ } | {
271
+ ok: false;
272
+ skipped: true;
273
+ }>;
219
274
  declare function createJmpContentClient(config: JmpContentConfig): {
220
275
  getBlogPosts: () => Promise<JmpBlogPost[]>;
221
276
  getBlogPost: (slug: string) => Promise<JmpBlogPost | null>;
@@ -223,6 +278,21 @@ declare function createJmpContentClient(config: JmpContentConfig): {
223
278
  getTrustBundle: () => Promise<JmpTrustBundle>;
224
279
  getSiteProfile: () => Promise<JmpSiteProfile>;
225
280
  getRobotsTxt: () => Promise<string>;
281
+ registerContentWrappers: () => Promise<{
282
+ ok: true;
283
+ wrappers: string[];
284
+ } | {
285
+ ok: false;
286
+ skipped: true;
287
+ }>;
288
+ /** @deprecated Use registerContentWrappers */
289
+ registerTrustCapabilities: () => Promise<{
290
+ ok: true;
291
+ slots: JmpTrustSlot[];
292
+ } | {
293
+ ok: false;
294
+ skipped: true;
295
+ }>;
226
296
  };
227
297
  /** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */
228
298
  declare const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
@@ -251,4 +321,4 @@ type JmpContentClient = ReturnType<typeof createJmpContentClient>;
251
321
  */
252
322
  declare function createJmpContentClientFromServerEnv(overrides?: Partial<JmpContentConfig>): JmpContentClient | null;
253
323
 
254
- export { DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, trackJmpServerPageView };
324
+ export { DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpContentWrapper, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustSlot, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, buildLandingCaptureInlineScript, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, registerJmpContentWrappers, registerJmpTrustCapabilities, sendEarlyPageViewBeacon, trackJmpServerPageView };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,18 @@
1
+ type EarlyPageViewBeaconArgs = {
2
+ baseUrl: string;
3
+ trackingKey: string;
4
+ };
5
+ /**
6
+ * Capture landing referrer/UTMs and send an early page_view (fetch keepalive).
7
+ * Safe to call from inline script or module init.
8
+ */
9
+ declare function sendEarlyPageViewBeacon(args: EarlyPageViewBeaconArgs): boolean;
10
+ /** Inline script for Next.js `beforeInteractive` — earliest capture + beacon. */
11
+ declare function buildLandingCaptureInlineScript(args: {
12
+ baseUrl: string;
13
+ trackingKey: string;
14
+ }): string;
15
+
1
16
  type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click";
2
17
  type JmpAnalyticsConfig = {
3
18
  /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */
@@ -14,7 +29,22 @@ type JmpAnalyticsConfig = {
14
29
  */
15
30
  pageViewDedupeMs?: number;
16
31
  };
17
- type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "debug" | "fetchImpl">;
32
+ type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "debug" | "fetchImpl"> & {
33
+ /**
34
+ * Content wrappers wired in this site's template (blog, faq, trust.*, contact).
35
+ * Reported via POST /api/content/capabilities so unused dashboard tabs stay hidden.
36
+ * Call {@link registerJmpContentWrappers} at startup / deploy.
37
+ */
38
+ contentWrappers?: JmpContentWrapper[];
39
+ /**
40
+ * @deprecated Use `contentWrappers` (e.g. `trust.testimonials` instead of `testimonials`).
41
+ */
42
+ trustSlots?: JmpTrustSlot[];
43
+ };
44
+ /** Top-level and trust sub-wrappers the client can declare. */
45
+ type JmpContentWrapper = "blog" | "faq" | "contact" | "trust" | "trust.summary" | "trust.testimonials" | "trust.reviews" | "trust.stats" | "trust.logos" | "trust.credentials";
46
+ /** @deprecated Trust-only shorthand; prefer `JmpContentWrapper`. */
47
+ type JmpTrustSlot = "summary" | "testimonials" | "reviews" | "stats" | "logos" | "credentials";
18
48
  type JmpBlogPost = {
19
49
  id: string;
20
50
  title: string;
@@ -51,11 +81,17 @@ type JmpTestimonial = {
51
81
  author_name: string;
52
82
  author_descriptor: string | null;
53
83
  location: string | null;
84
+ /** 1–10 (half-star steps on a 5-star scale; 10 = 5.0 stars). */
54
85
  rating: number | null;
55
86
  image_url: string | null;
56
87
  video_url: string | null;
88
+ /** Optional topic or service line (e.g. roofing). */
57
89
  category: string | null;
90
+ /** Site section key — filter like FAQ `category` (e.g. testimonials, reviews). */
91
+ placement: string | null;
58
92
  source: string | null;
93
+ /** Optional link to the original review on Google, Yelp, etc. */
94
+ source_url: string | null;
59
95
  featured: boolean;
60
96
  sort_order: number;
61
97
  updated_at: string;
@@ -216,6 +252,25 @@ declare function getJmpFaqs(config: JmpContentConfig): Promise<JmpFaqItem[]>;
216
252
  declare function getJmpTrustBundle(config: JmpContentConfig): Promise<JmpTrustBundle>;
217
253
  declare function getJmpSiteProfile(config: JmpContentConfig): Promise<JmpSiteProfile>;
218
254
  declare function getJmpRobotsTxt(config: JmpContentConfig): Promise<string>;
255
+ /**
256
+ * Tell the JMP dashboard which content wrappers this site wires (blog, faq, trust.*, contact).
257
+ * Safe to call on every cold start; dedupes by tracking key + wrapper list.
258
+ */
259
+ declare function registerJmpContentWrappers(config: JmpContentConfig): Promise<{
260
+ ok: true;
261
+ wrappers: string[];
262
+ } | {
263
+ ok: false;
264
+ skipped: true;
265
+ }>;
266
+ /** @deprecated Use {@link registerJmpContentWrappers} with `contentWrappers`. */
267
+ declare function registerJmpTrustCapabilities(config: JmpContentConfig): Promise<{
268
+ ok: true;
269
+ slots: JmpTrustSlot[];
270
+ } | {
271
+ ok: false;
272
+ skipped: true;
273
+ }>;
219
274
  declare function createJmpContentClient(config: JmpContentConfig): {
220
275
  getBlogPosts: () => Promise<JmpBlogPost[]>;
221
276
  getBlogPost: (slug: string) => Promise<JmpBlogPost | null>;
@@ -223,6 +278,21 @@ declare function createJmpContentClient(config: JmpContentConfig): {
223
278
  getTrustBundle: () => Promise<JmpTrustBundle>;
224
279
  getSiteProfile: () => Promise<JmpSiteProfile>;
225
280
  getRobotsTxt: () => Promise<string>;
281
+ registerContentWrappers: () => Promise<{
282
+ ok: true;
283
+ wrappers: string[];
284
+ } | {
285
+ ok: false;
286
+ skipped: true;
287
+ }>;
288
+ /** @deprecated Use registerContentWrappers */
289
+ registerTrustCapabilities: () => Promise<{
290
+ ok: true;
291
+ slots: JmpTrustSlot[];
292
+ } | {
293
+ ok: false;
294
+ skipped: true;
295
+ }>;
226
296
  };
227
297
  /** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */
228
298
  declare const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
@@ -251,4 +321,4 @@ type JmpContentClient = ReturnType<typeof createJmpContentClient>;
251
321
  */
252
322
  declare function createJmpContentClientFromServerEnv(overrides?: Partial<JmpContentConfig>): JmpContentClient | null;
253
323
 
254
- export { DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, trackJmpServerPageView };
324
+ export { DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpContentWrapper, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustSlot, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, buildLandingCaptureInlineScript, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, registerJmpContentWrappers, registerJmpTrustCapabilities, sendEarlyPageViewBeacon, trackJmpServerPageView };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
3
+ buildLandingCaptureInlineScript,
3
4
  createJmpAnalytics,
4
5
  createJmpContentClient,
5
6
  createJmpContentClientFromServerEnv,
@@ -14,10 +15,14 @@ import {
14
15
  getOrCreateJmpAnalyticsFromBrowserEnv,
15
16
  isJmpBrowserAnalyticsEnvConfigured,
16
17
  isJmpServerContentEnvConfigured,
18
+ registerJmpContentWrappers,
19
+ registerJmpTrustCapabilities,
20
+ sendEarlyPageViewBeacon,
17
21
  trackJmpServerPageView
18
- } from "./chunk-K47RWY4L.js";
22
+ } from "./chunk-CCCYRBYK.js";
19
23
  export {
20
24
  DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
25
+ buildLandingCaptureInlineScript,
21
26
  createJmpAnalytics,
22
27
  createJmpContentClient,
23
28
  createJmpContentClientFromServerEnv,
@@ -32,6 +37,9 @@ export {
32
37
  getOrCreateJmpAnalyticsFromBrowserEnv,
33
38
  isJmpBrowserAnalyticsEnvConfigured,
34
39
  isJmpServerContentEnvConfigured,
40
+ registerJmpContentWrappers,
41
+ registerJmpTrustCapabilities,
42
+ sendEarlyPageViewBeacon,
35
43
  trackJmpServerPageView
36
44
  };
37
45
  //# sourceMappingURL=index.js.map
package/dist/next.cjs CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  "use client";
3
+ var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
9
  var __export = (target, all) => {
8
10
  for (var name in all)
@@ -16,52 +18,237 @@ var __copyProps = (to, from, except, desc) => {
16
18
  }
17
19
  return to;
18
20
  };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
19
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
30
 
21
31
  // src/next.tsx
22
32
  var next_exports = {};
23
33
  __export(next_exports, {
34
+ JmpLandingCaptureScript: () => JmpLandingCaptureScript,
24
35
  JmpNextAnalytics: () => JmpNextAnalytics,
25
36
  JmpNextPageViewTracker: () => JmpNextPageViewTracker,
26
37
  JmpTrackedMailtoLink: () => JmpTrackedMailtoLink,
27
38
  JmpTrackedTelLink: () => JmpTrackedTelLink,
39
+ buildLandingCaptureInlineScript: () => buildLandingCaptureInlineScript,
28
40
  useJmpAnalytics: () => useJmpAnalytics
29
41
  });
30
42
  module.exports = __toCommonJS(next_exports);
43
+ var import_script = __toESM(require("next/script"), 1);
31
44
  var import_react2 = require("react");
32
45
  var import_navigation = require("next/navigation");
33
46
 
34
- // src/index.ts
35
- var DEFAULT_SOURCE = "direct";
36
- var SESSION_STORAGE_KEY = "jmp_analytics_sid";
37
- function responseLooksLikeHtml(body) {
38
- const s = body.trimStart();
39
- return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
47
+ // src/landing-capture.ts
48
+ var JMP_SESSION_ID_KEY = "jmp_analytics_sid";
49
+ var JMP_LANDING_REFERER_KEY = "jmp_analytics_landing_referer";
50
+ var JMP_UTM_SESSION_KEY = "jmp_analytics_utm";
51
+ var JMP_EARLY_BEACON_PATH_KEY = "jmp_analytics_early_beacon_path";
52
+ var JMP_UTM_PARAM_KEYS = [
53
+ "utm_source",
54
+ "utm_medium",
55
+ "utm_campaign",
56
+ "utm_content",
57
+ "utm_term"
58
+ ];
59
+ function refererHostFromUrl(referer) {
60
+ const trimmed = referer.trim();
61
+ try {
62
+ return new URL(trimmed).hostname.replace(/^www\./i, "").toLowerCase();
63
+ } catch {
64
+ try {
65
+ return new URL(`https://${trimmed}`).hostname.replace(/^www\./i, "").toLowerCase();
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
40
70
  }
41
- function normalizeBaseUrl(baseUrl) {
42
- return baseUrl.replace(/\/$/, "");
71
+ function isExternalReferrerUrl(referer, siteHostname) {
72
+ const refHost = refererHostFromUrl(referer);
73
+ if (!refHost) {
74
+ return false;
75
+ }
76
+ const site = siteHostname.replace(/^www\./i, "").toLowerCase();
77
+ return refHost !== site && !refHost.endsWith(`.${site}`);
43
78
  }
44
- function inferDevice() {
45
- if (typeof window === "undefined") {
46
- return "unknown";
79
+ function captureLandingAttribution(siteHostname) {
80
+ if (typeof document === "undefined" || typeof window === "undefined") {
81
+ return { landingReferer: null, utm: {} };
47
82
  }
48
- return window.innerWidth < 768 ? "mobile" : "desktop";
83
+ let landingReferer = null;
84
+ let utm = {};
85
+ try {
86
+ const storedReferer = sessionStorage.getItem(JMP_LANDING_REFERER_KEY)?.trim();
87
+ const currentReferer = document.referrer?.trim();
88
+ if (currentReferer && isExternalReferrerUrl(currentReferer, siteHostname)) {
89
+ if (!storedReferer) {
90
+ sessionStorage.setItem(JMP_LANDING_REFERER_KEY, currentReferer);
91
+ }
92
+ landingReferer = storedReferer || currentReferer;
93
+ } else if (storedReferer) {
94
+ landingReferer = storedReferer;
95
+ }
96
+ const params = new URLSearchParams(window.location.search);
97
+ const rawUtm = sessionStorage.getItem(JMP_UTM_SESSION_KEY);
98
+ if (rawUtm) {
99
+ const parsed = JSON.parse(rawUtm);
100
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
101
+ utm = parsed;
102
+ }
103
+ }
104
+ let updated = false;
105
+ for (const key of JMP_UTM_PARAM_KEYS) {
106
+ const value = params.get(key)?.trim();
107
+ if (value) {
108
+ utm[key] = value;
109
+ updated = true;
110
+ }
111
+ }
112
+ if (updated) {
113
+ sessionStorage.setItem(JMP_UTM_SESSION_KEY, JSON.stringify(utm));
114
+ }
115
+ } catch {
116
+ }
117
+ return { landingReferer, utm };
49
118
  }
50
- function getOrCreateSessionId() {
119
+ function getOrCreateSessionIdFromStorage() {
51
120
  if (typeof window === "undefined" || !globalThis.crypto?.randomUUID) {
52
121
  return void 0;
53
122
  }
54
123
  try {
55
- let id = sessionStorage.getItem(SESSION_STORAGE_KEY);
124
+ let id = sessionStorage.getItem(JMP_SESSION_ID_KEY);
56
125
  if (!id) {
57
126
  id = crypto.randomUUID();
58
- sessionStorage.setItem(SESSION_STORAGE_KEY, id);
127
+ sessionStorage.setItem(JMP_SESSION_ID_KEY, id);
59
128
  }
60
129
  return id;
61
130
  } catch {
62
131
  return void 0;
63
132
  }
64
133
  }
134
+ function wasEarlyBeaconSentForPath(path) {
135
+ if (typeof window === "undefined") {
136
+ return false;
137
+ }
138
+ try {
139
+ return sessionStorage.getItem(JMP_EARLY_BEACON_PATH_KEY) === path;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ function markEarlyBeaconSentForPath(path) {
145
+ if (typeof window === "undefined") {
146
+ return;
147
+ }
148
+ try {
149
+ sessionStorage.setItem(JMP_EARLY_BEACON_PATH_KEY, path);
150
+ } catch {
151
+ }
152
+ }
153
+ function sendEarlyPageViewBeacon(args) {
154
+ if (typeof window === "undefined" || typeof fetch === "undefined") {
155
+ return false;
156
+ }
157
+ const path = `${window.location.pathname}${window.location.search}` || "/";
158
+ if (wasEarlyBeaconSentForPath(path)) {
159
+ return false;
160
+ }
161
+ const { landingReferer, utm } = captureLandingAttribution(window.location.hostname);
162
+ const sessionId = getOrCreateSessionIdFromStorage();
163
+ const metadata = {
164
+ pageHostname: window.location.hostname,
165
+ earlyBeacon: true,
166
+ ...utm
167
+ };
168
+ if (landingReferer) {
169
+ metadata.referer = landingReferer;
170
+ }
171
+ const payload = {
172
+ type: "page_view",
173
+ path,
174
+ trackingKey: args.trackingKey,
175
+ ...sessionId ? { sessionId } : {},
176
+ metadata
177
+ };
178
+ const headers = {
179
+ "Content-Type": "application/json",
180
+ "X-JMP-Tracking-Key": args.trackingKey
181
+ };
182
+ if (landingReferer) {
183
+ headers["X-JMP-Visitor-Referer"] = landingReferer;
184
+ }
185
+ markEarlyBeaconSentForPath(path);
186
+ void fetch(`${args.baseUrl.replace(/\/$/, "")}/api/events`, {
187
+ method: "POST",
188
+ headers,
189
+ body: JSON.stringify(payload),
190
+ keepalive: true,
191
+ mode: "cors",
192
+ credentials: "omit"
193
+ }).catch(() => {
194
+ try {
195
+ sessionStorage.removeItem(JMP_EARLY_BEACON_PATH_KEY);
196
+ } catch {
197
+ }
198
+ });
199
+ return true;
200
+ }
201
+ function escapeInlineScript(value) {
202
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
203
+ }
204
+ function buildLandingCaptureInlineScript(args) {
205
+ const baseUrl = escapeInlineScript(args.baseUrl.replace(/\/$/, ""));
206
+ const trackingKey = escapeInlineScript(args.trackingKey);
207
+ return `(function(){try{var k='${trackingKey}',b='${baseUrl}';if(!k||!b)return;var p=location.pathname+location.search||'/';var sentKey='${JMP_EARLY_BEACON_PATH_KEY}';if(sessionStorage.getItem(sentKey)===p)return;var h=location.hostname;var ref=document.referrer||'';function rh(u){try{return new URL(u).hostname.replace(/^www\\./i,'').toLowerCase()}catch(e){try{return new URL('https://'+u).hostname.replace(/^www\\./i,'').toLowerCase()}catch(e2){return null}}}function ext(r){var x=rh(r);if(!x)return false;var s=h.replace(/^www\\./i,'').toLowerCase();return x!==s&&x.slice(-('.'+s).length)!==('.'+s)}if(ref&&ext(ref)&&!sessionStorage.getItem('${JMP_LANDING_REFERER_KEY}'))sessionStorage.setItem('${JMP_LANDING_REFERER_KEY}',ref);var lr=sessionStorage.getItem('${JMP_LANDING_REFERER_KEY}')||'';var sid=sessionStorage.getItem('${JMP_SESSION_ID_KEY}');if(!sid&&crypto&&crypto.randomUUID){sid=crypto.randomUUID();sessionStorage.setItem('${JMP_SESSION_ID_KEY}',sid)}var md={pageHostname:h,earlyBeacon:true};try{var sp=new URLSearchParams(location.search);['utm_source','utm_medium','utm_campaign','utm_content','utm_term'].forEach(function(n){var v=sp.get(n);if(v)md[n]=v})}catch(e){}if(lr)md.referer=lr;var body=JSON.stringify({type:'page_view',path:p,trackingKey:k,sessionId:sid||undefined,metadata:md});var hd={'Content-Type':'application/json','X-JMP-Tracking-Key':k};if(lr)hd['X-JMP-Visitor-Referer']=lr;sessionStorage.setItem(sentKey,p);fetch(b+'/api/events',{method:'POST',headers:hd,body:body,keepalive:true,mode:'cors',credentials:'omit'}).catch(function(){sessionStorage.removeItem(sentKey)})}catch(e){}})();`;
208
+ }
209
+
210
+ // src/index.ts
211
+ var DEFAULT_SOURCE = "direct";
212
+ function captureAndGetLandingReferer() {
213
+ if (typeof window === "undefined") {
214
+ return null;
215
+ }
216
+ return captureLandingAttribution(window.location.hostname).landingReferer;
217
+ }
218
+ function mergeSessionUtms(metadata) {
219
+ if (typeof window === "undefined") {
220
+ return;
221
+ }
222
+ const { utm } = captureLandingAttribution(window.location.hostname);
223
+ for (const key of JMP_UTM_PARAM_KEYS) {
224
+ if (metadata[key] === void 0 && utm[key]) {
225
+ metadata[key] = utm[key];
226
+ }
227
+ }
228
+ }
229
+ function splitPathAndSearch(pathInput) {
230
+ const qIndex = pathInput.indexOf("?");
231
+ if (qIndex === -1) {
232
+ return { pathname: pathInput || "/", search: "" };
233
+ }
234
+ return {
235
+ pathname: pathInput.slice(0, qIndex) || "/",
236
+ search: pathInput.slice(qIndex + 1)
237
+ };
238
+ }
239
+ function responseLooksLikeHtml(body) {
240
+ const s = body.trimStart();
241
+ return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
242
+ }
243
+ function normalizeBaseUrl(baseUrl) {
244
+ return baseUrl.replace(/\/$/, "");
245
+ }
246
+ function inferDevice() {
247
+ if (typeof window === "undefined") {
248
+ return "unknown";
249
+ }
250
+ return window.innerWidth < 768 ? "mobile" : "desktop";
251
+ }
65
252
  function createJmpAnalytics(config) {
66
253
  const baseUrl = normalizeBaseUrl(config.baseUrl);
67
254
  const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -79,7 +266,13 @@ function createJmpAnalytics(config) {
79
266
  "Content-Type": "application/json",
80
267
  "X-JMP-Tracking-Key": config.trackingKey
81
268
  };
82
- const sid = getOrCreateSessionId();
269
+ if (body.type === "page_view" && typeof window !== "undefined") {
270
+ const landingReferer = captureAndGetLandingReferer();
271
+ if (landingReferer) {
272
+ headers["X-JMP-Visitor-Referer"] = landingReferer;
273
+ }
274
+ }
275
+ const sid = getOrCreateSessionIdFromStorage();
83
276
  const payload = { ...body, ...sid ? { sessionId: sid } : {} };
84
277
  if (config.debug) {
85
278
  console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
@@ -128,7 +321,7 @@ function createJmpAnalytics(config) {
128
321
  "Content-Type": "application/json",
129
322
  "X-JMP-Tracking-Key": config.trackingKey
130
323
  };
131
- const sid = getOrCreateSessionId();
324
+ const sid = getOrCreateSessionIdFromStorage();
132
325
  const payload = { ...body, ...sid ? { sessionId: sid } : {} };
133
326
  if (config.debug) {
134
327
  console.debug("[@jmp-technologies/analytics]", "POST", url, payload);
@@ -185,28 +378,44 @@ function createJmpAnalytics(config) {
185
378
  return false;
186
379
  }
187
380
  function trackPageView(options) {
188
- const pathname = options?.path ?? (typeof window !== "undefined" ? window.location.pathname : "/");
189
- const search = typeof window !== "undefined" ? window.location.search.replace(/^\?/, "") : "";
381
+ const pathFromOptions = options?.path?.trim();
382
+ let pathname = "/";
383
+ let search = "";
384
+ if (pathFromOptions) {
385
+ const split = splitPathAndSearch(pathFromOptions);
386
+ pathname = split.pathname;
387
+ search = split.search;
388
+ } else if (typeof window !== "undefined") {
389
+ pathname = window.location.pathname;
390
+ search = window.location.search.replace(/^\?/, "");
391
+ }
392
+ if (!search && typeof window !== "undefined") {
393
+ search = window.location.search.replace(/^\?/, "");
394
+ }
190
395
  const path = search ? `${pathname}?${search}` : pathname;
396
+ if (wasEarlyBeaconSentForPath(path)) {
397
+ return Promise.resolve();
398
+ }
191
399
  if (shouldDedupePageView(pathname)) {
192
400
  return Promise.resolve();
193
401
  }
194
402
  const metadata = options?.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
195
403
  if (typeof window !== "undefined") {
196
404
  metadata.pageHostname = window.location.hostname;
197
- const params = new URLSearchParams(window.location.search);
198
- for (const key of [
199
- "utm_source",
200
- "utm_medium",
201
- "utm_campaign",
202
- "utm_content",
203
- "utm_term"
204
- ]) {
405
+ const params = new URLSearchParams(
406
+ search ? `?${search}` : window.location.search
407
+ );
408
+ for (const key of JMP_UTM_PARAM_KEYS) {
205
409
  const v = params.get(key);
206
410
  if (v && metadata[key] === void 0) {
207
411
  metadata[key] = v;
208
412
  }
209
413
  }
414
+ mergeSessionUtms(metadata);
415
+ const landingReferer = captureAndGetLandingReferer();
416
+ if (landingReferer && metadata.referer === void 0) {
417
+ metadata.referer = landingReferer;
418
+ }
210
419
  }
211
420
  return send({
212
421
  type: "page_view",
@@ -304,6 +513,13 @@ function createJmpAnalytics(config) {
304
513
  history.replaceState = replace;
305
514
  };
306
515
  }
516
+ if (typeof window !== "undefined") {
517
+ captureLandingAttribution(window.location.hostname);
518
+ sendEarlyPageViewBeacon({
519
+ baseUrl,
520
+ trackingKey: config.trackingKey
521
+ });
522
+ }
307
523
  return {
308
524
  track,
309
525
  trackPageView,
@@ -388,12 +604,35 @@ function JmpTrackedMailtoLink({
388
604
 
389
605
  // src/next.tsx
390
606
  var import_jsx_runtime2 = require("react/jsx-runtime");
607
+ function JmpLandingCaptureScript({
608
+ baseUrl = process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL,
609
+ trackingKey = process.env.NEXT_PUBLIC_JMP_TRACKING_KEY
610
+ } = {}) {
611
+ const resolvedBase = baseUrl?.trim();
612
+ const resolvedKey = trackingKey?.trim();
613
+ if (!resolvedBase || !resolvedKey) {
614
+ return null;
615
+ }
616
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
617
+ import_script.default,
618
+ {
619
+ id: "jmp-landing-capture",
620
+ strategy: "beforeInteractive",
621
+ dangerouslySetInnerHTML: {
622
+ __html: buildLandingCaptureInlineScript({
623
+ baseUrl: resolvedBase,
624
+ trackingKey: resolvedKey
625
+ })
626
+ }
627
+ }
628
+ );
629
+ }
391
630
  function JmpNextPageViewTrackerInner() {
392
631
  const pathname = (0, import_navigation.usePathname)();
393
632
  const searchParams = (0, import_navigation.useSearchParams)();
394
633
  const query = searchParams?.toString() ?? "";
395
634
  const path = query ? `${pathname}?${query}` : pathname;
396
- (0, import_react2.useEffect)(() => {
635
+ (0, import_react2.useLayoutEffect)(() => {
397
636
  const client = getOrCreateJmpAnalyticsFromBrowserEnv();
398
637
  void client?.trackPageView({ path });
399
638
  }, [path]);
@@ -403,14 +642,19 @@ function JmpNextPageViewTracker() {
403
642
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(JmpNextPageViewTrackerInner, {}) });
404
643
  }
405
644
  function JmpNextAnalytics() {
406
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(JmpNextPageViewTracker, {});
645
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
646
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(JmpLandingCaptureScript, {}),
647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(JmpNextPageViewTracker, {})
648
+ ] });
407
649
  }
408
650
  // Annotate the CommonJS export names for ESM import in node:
409
651
  0 && (module.exports = {
652
+ JmpLandingCaptureScript,
410
653
  JmpNextAnalytics,
411
654
  JmpNextPageViewTracker,
412
655
  JmpTrackedMailtoLink,
413
656
  JmpTrackedTelLink,
657
+ buildLandingCaptureInlineScript,
414
658
  useJmpAnalytics
415
659
  });
416
660
  //# sourceMappingURL=next.cjs.map