@nextlytics/core 0.3.0-canary.70 → 0.3.0-canary.80

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.
@@ -0,0 +1,11 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { NextlyticsEvent, RequestContext, PageViewDelivery, DispatchResult, UserContext } from './types.js';
3
+ import { NextlyticsConfigWithDefaults } from './config-helpers.js';
4
+ import 'next/dist/server/web/spec-extension/cookies';
5
+
6
+ type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext, policyFilter?: PageViewDelivery | "client-actions") => DispatchResult;
7
+ type UpdateEvent = (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>;
8
+ declare function getUserContext(config: NextlyticsConfigWithDefaults, ctx: RequestContext): Promise<UserContext | undefined>;
9
+ declare function handleEventPost(request: NextRequest, config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): Promise<Response>;
10
+
11
+ export { type DispatchEvent, type UpdateEvent, getUserContext, handleEventPost };
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var api_handler_exports = {};
20
+ __export(api_handler_exports, {
21
+ getUserContext: () => getUserContext,
22
+ handleEventPost: () => handleEventPost
23
+ });
24
+ module.exports = __toCommonJS(api_handler_exports);
25
+ var import_server = require("next/server");
26
+ var import_server_component_context = require("./server-component-context");
27
+ var import_uitils = require("./uitils");
28
+ var import_anonymous_user = require("./anonymous-user");
29
+ function createRequestContext(request) {
30
+ return {
31
+ headers: request.headers,
32
+ cookies: request.cookies
33
+ };
34
+ }
35
+ async function getUserContext(config, ctx) {
36
+ if (!config.callbacks.getUser) return void 0;
37
+ try {
38
+ return await config.callbacks.getUser(ctx) || void 0;
39
+ } catch {
40
+ return void 0;
41
+ }
42
+ }
43
+ function reconstructServerContext(apiCallContext, clientContext) {
44
+ const searchParams = {};
45
+ if (clientContext.search) {
46
+ const params = new URLSearchParams(clientContext.search);
47
+ params.forEach((value, key) => {
48
+ if (!searchParams[key]) searchParams[key] = [];
49
+ searchParams[key].push(value);
50
+ });
51
+ }
52
+ return {
53
+ ...apiCallContext,
54
+ host: clientContext.host || apiCallContext.host,
55
+ path: clientContext.path || apiCallContext.path,
56
+ search: Object.keys(searchParams).length > 0 ? searchParams : apiCallContext.search,
57
+ method: "GET"
58
+ };
59
+ }
60
+ function filterScripts(actions) {
61
+ const scripts = actions.items.filter((i) => i.type === "script-template");
62
+ return scripts.length > 0 ? scripts : void 0;
63
+ }
64
+ async function handleClientInit(request, hctx) {
65
+ const {
66
+ pageRenderId,
67
+ ctx,
68
+ apiCallServerContext,
69
+ userContext,
70
+ config,
71
+ dispatchEvent,
72
+ updateEvent
73
+ } = hctx;
74
+ const { clientContext } = request;
75
+ const serverContext = reconstructServerContext(apiCallServerContext, clientContext);
76
+ const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
77
+ ctx,
78
+ serverContext,
79
+ config
80
+ });
81
+ const isSoftNavigation = hctx.isSoftNavigation;
82
+ const eventId = isSoftNavigation ? (0, import_uitils.generateId)() : pageRenderId;
83
+ const event = {
84
+ origin: "client",
85
+ eventId,
86
+ parentEventId: isSoftNavigation ? pageRenderId : void 0,
87
+ type: "pageView",
88
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
89
+ anonymousUserId,
90
+ serverContext,
91
+ clientContext,
92
+ userContext,
93
+ properties: {}
94
+ };
95
+ if (isSoftNavigation) {
96
+ const { clientActions, completion: completion2 } = dispatchEvent(event, ctx);
97
+ const actions = await clientActions;
98
+ (0, import_server.after)(() => completion2);
99
+ return Response.json({
100
+ ok: true,
101
+ items: filterScripts(actions)
102
+ });
103
+ }
104
+ const { completion } = dispatchEvent(event, ctx, "client-actions");
105
+ (0, import_server.after)(() => completion);
106
+ (0, import_server.after)(() => updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx));
107
+ return Response.json({ ok: true });
108
+ }
109
+ async function handleClientEvent(request, hctx) {
110
+ const { pageRenderId, ctx, apiCallServerContext, userContext, config, dispatchEvent } = hctx;
111
+ const { clientContext, name, props, collectedAt } = request;
112
+ const serverContext = clientContext ? reconstructServerContext(apiCallServerContext, clientContext) : apiCallServerContext;
113
+ const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
114
+ ctx,
115
+ serverContext,
116
+ config
117
+ });
118
+ const event = {
119
+ origin: "client",
120
+ eventId: (0, import_uitils.generateId)(),
121
+ parentEventId: pageRenderId,
122
+ type: name,
123
+ collectedAt: collectedAt || (/* @__PURE__ */ new Date()).toISOString(),
124
+ anonymousUserId,
125
+ serverContext,
126
+ clientContext,
127
+ userContext,
128
+ properties: props || {}
129
+ };
130
+ const { clientActions, completion } = dispatchEvent(event, ctx);
131
+ const actions = await clientActions;
132
+ (0, import_server.after)(() => completion);
133
+ return Response.json({ ok: true, items: filterScripts(actions) });
134
+ }
135
+ async function handleEventPost(request, config, dispatchEvent, updateEvent) {
136
+ const softNavHeader = request.headers.get(import_server_component_context.headerNames.isSoftNavigation);
137
+ const isSoftNavigation = softNavHeader === "1";
138
+ const pageRenderIdHeader = request.headers.get(import_server_component_context.headerNames.pageRenderId);
139
+ if (!pageRenderIdHeader) {
140
+ return Response.json({ error: "Missing page render ID" }, { status: 400 });
141
+ }
142
+ let body;
143
+ try {
144
+ body = await request.json();
145
+ } catch {
146
+ return Response.json({ error: "Invalid JSON" }, { status: 400 });
147
+ }
148
+ const ctx = createRequestContext(request);
149
+ const apiCallServerContext = (0, import_uitils.createServerContext)(request);
150
+ const userContext = await getUserContext(config, ctx);
151
+ const cookiePageRenderId = request.cookies.get(import_server_component_context.LAST_PAGE_RENDER_ID_COOKIE)?.value;
152
+ const pageRenderId = isSoftNavigation ? cookiePageRenderId ?? (0, import_uitils.generateId)() : pageRenderIdHeader;
153
+ if (isSoftNavigation && !cookiePageRenderId && config.debug) {
154
+ console.warn(
155
+ "[Nextlytics] Missing last-page-render-id cookie on soft navigation; using a new id."
156
+ );
157
+ }
158
+ const hctx = {
159
+ pageRenderId,
160
+ isSoftNavigation,
161
+ ctx,
162
+ apiCallServerContext,
163
+ userContext,
164
+ config,
165
+ dispatchEvent,
166
+ updateEvent
167
+ };
168
+ const bodyType = body.type;
169
+ switch (bodyType) {
170
+ case "page-view":
171
+ return handleClientInit(body, hctx);
172
+ case "custom-event":
173
+ return handleClientEvent(body, hctx);
174
+ default:
175
+ return Response.json({ ok: false, error: `Unknown body type ${bodyType}` }, { status: 400 });
176
+ }
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ getUserContext,
181
+ handleEventPost
182
+ });
@@ -15,6 +15,11 @@ type GoogleAnalyticsBackendOptions = {
15
15
  * - "anonymousUserId": Always use Nextlytics anonymousUserId
16
16
  */
17
17
  clientIdSource?: "gaCookie" | "anonymousUserId";
18
+ /**
19
+ * Prefer sending client-origin events from the browser (gtag) instead of Measurement Protocol.
20
+ * Default: true. Set to false to force Measurement Protocol when apiSecret is provided.
21
+ */
22
+ preferClientSideForClientEvents?: boolean;
18
23
  };
19
24
  declare function googleAnalyticsBackend(opts: GoogleAnalyticsBackendOptions): NextlyticsBackendFactory;
20
25
 
@@ -21,7 +21,9 @@ __export(ga_exports, {
21
21
  googleAnalyticsBackend: () => googleAnalyticsBackend
22
22
  });
23
23
  module.exports = __toCommonJS(ga_exports);
24
- const GA_TEMPLATE_ID = "ga-gtag";
24
+ const GA_INIT_TEMPLATE = "ga-gtag";
25
+ const GA_PROPERTIES_TEMPLATE = "ga-properties";
26
+ const GA_EVENT_TEMPLATE = "ga-event";
25
27
  function parseGaCookie(cookieValue) {
26
28
  const match = cookieValue.match(/^GA\d+\.\d+\.(.+)$/);
27
29
  return match ? match[1] : null;
@@ -118,27 +120,51 @@ function googleAnalyticsBackend(opts) {
118
120
  return (ctx) => {
119
121
  const gaCookie = ctx.cookies.get("_ga");
120
122
  const gaCookieClientId = gaCookie ? parseGaCookie(gaCookie.value) : null;
123
+ const preferClientSideForClientEvents = opts.preferClientSideForClientEvents ?? true;
121
124
  return {
122
125
  name: "google-analytics",
123
126
  returnsClientActions: true,
124
127
  supportsUpdates: false,
125
128
  getClientSideTemplates() {
126
129
  return {
127
- [GA_TEMPLATE_ID]: {
130
+ [GA_EVENT_TEMPLATE]: {
131
+ deps: "{{eventId}}",
128
132
  items: [
133
+ // Update user properties for this event (if provided)
134
+ {
135
+ body: [
136
+ "gtag('set', {{json(properties)}});",
137
+ "gtag('event', '{{eventName}}', {{json(eventParams)}});"
138
+ ]
139
+ }
140
+ ]
141
+ },
142
+ [GA_INIT_TEMPLATE]: {
143
+ deps: "{{measurementId}}{{json(initial_config)}}",
144
+ items: [
145
+ // External gtag.js - load once
129
146
  {
130
- async: "true",
131
147
  src: "https://www.googletagmanager.com/gtag/js?id={{measurementId}}",
132
- singleton: true
148
+ async: true
133
149
  },
150
+ // gtag definition and initialization - run once
134
151
  {
135
152
  body: [
136
153
  "window.dataLayer = window.dataLayer || [];",
137
- "function gtag(){dataLayer.push(arguments);}",
154
+ opts.debugMode ? "function gtag(){ console.log('[gtag() call]', arguments); dataLayer.push(arguments); }" : "function gtag(){dataLayer.push(arguments);}",
155
+ "window.gtag = gtag;",
138
156
  "gtag('js', new Date());",
139
- "gtag('config', '{{measurementId}}', {{json(config)}});",
140
- "gtag('event', 'page_view');"
141
- ].join("\n")
157
+ "gtag('config', '{{measurementId}}', {{json(initial_config)}});"
158
+ ]
159
+ }
160
+ ]
161
+ },
162
+ [GA_PROPERTIES_TEMPLATE]: {
163
+ deps: "{{json(properties)}}",
164
+ items: [
165
+ // Updates that should NOT trigger page_view (e.g., user_id, user_properties)
166
+ {
167
+ body: "gtag('set', {{json(properties)}});"
142
168
  }
143
169
  ]
144
170
  }
@@ -155,33 +181,66 @@ function googleAnalyticsBackend(opts) {
155
181
  } = event.userContext?.traits ?? {};
156
182
  const userProperties = Object.keys(customTraits).length > 0 ? customTraits : void 0;
157
183
  if (event.type === "pageView") {
158
- const config = {
159
- send_page_view: false,
184
+ const initial_config = {
185
+ // Rely on GA auto page_view (including SPA history changes).
186
+ send_page_view: true,
160
187
  client_id: clientId
161
188
  };
162
189
  if (debugMode) {
163
- config.debug_mode = true;
190
+ initial_config.debug_mode = true;
164
191
  }
192
+ const properties2 = {};
165
193
  if (userId) {
166
- config.user_id = userId;
194
+ properties2.user_id = userId;
167
195
  }
168
196
  if (userProperties) {
169
- config.user_properties = userProperties;
197
+ properties2.user_properties = userProperties;
170
198
  }
171
199
  return {
172
200
  items: [
173
201
  {
174
202
  type: "script-template",
175
- templateId: GA_TEMPLATE_ID,
203
+ templateId: GA_INIT_TEMPLATE,
176
204
  params: {
177
205
  measurementId,
178
- config
206
+ initial_config
207
+ }
208
+ },
209
+ {
210
+ type: "script-template",
211
+ templateId: GA_PROPERTIES_TEMPLATE,
212
+ params: {
213
+ properties: properties2
179
214
  }
180
215
  }
181
216
  ]
182
217
  };
183
218
  }
184
- if (apiSecret) {
219
+ const eventParams = buildEventParams(event);
220
+ const properties = {};
221
+ if (userId) {
222
+ properties.user_id = userId;
223
+ }
224
+ if (userProperties) {
225
+ properties.user_properties = userProperties;
226
+ }
227
+ if (event.origin === "client") {
228
+ if (preferClientSideForClientEvents || !apiSecret) {
229
+ return {
230
+ items: [
231
+ {
232
+ type: "script-template",
233
+ templateId: GA_EVENT_TEMPLATE,
234
+ params: {
235
+ eventId: event.eventId,
236
+ eventName: toGA4EventName(event.type),
237
+ eventParams,
238
+ properties
239
+ }
240
+ }
241
+ ]
242
+ };
243
+ }
185
244
  await sendToMeasurementProtocol({
186
245
  measurementId,
187
246
  apiSecret,
@@ -189,12 +248,29 @@ function googleAnalyticsBackend(opts) {
189
248
  userId,
190
249
  userProperties,
191
250
  eventName: toGA4EventName(event.type),
192
- eventParams: buildEventParams(event),
251
+ eventParams,
193
252
  userAgent: getUserAgent(event),
194
253
  clientIp: getClientIp(event),
195
254
  debugMode
196
255
  });
256
+ return void 0;
257
+ }
258
+ if (!apiSecret) {
259
+ return void 0;
197
260
  }
261
+ await sendToMeasurementProtocol({
262
+ measurementId,
263
+ apiSecret,
264
+ clientId,
265
+ userId,
266
+ userProperties,
267
+ eventName: toGA4EventName(event.type),
268
+ eventParams,
269
+ userAgent: getUserAgent(event),
270
+ clientIp: getClientIp(event),
271
+ debugMode
272
+ });
273
+ return void 0;
198
274
  },
199
275
  updateEvent() {
200
276
  }
@@ -22,6 +22,7 @@ __export(gtm_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(gtm_exports);
24
24
  const GTM_INIT_TEMPLATE_ID = "gtm-init";
25
+ const GTM_INIT_DATA_TEMPLATE_ID = "gtm-init-data";
25
26
  const GTM_PAGEVIEW_TEMPLATE_ID = "gtm-pageview";
26
27
  const GTM_EVENT_TEMPLATE_ID = "gtm-event";
27
28
  function toSnakeCase(str) {
@@ -37,21 +38,27 @@ function googleTagManagerBackend(opts) {
37
38
  return {
38
39
  [GTM_INIT_TEMPLATE_ID]: {
39
40
  items: [
41
+ // GTM script loader - run once
40
42
  {
41
43
  body: [
42
44
  "window.dataLayer = window.dataLayer || [];",
43
- "dataLayer.push({{json(initialData)}});",
44
- "if (!window.google_tag_manager || !window.google_tag_manager['{{containerId}}']) {",
45
- " (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':",
46
- " new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],",
47
- " j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=",
48
- " 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);",
49
- " })(window,document,'script','dataLayer','{{containerId}}');",
50
- "}"
45
+ "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':",
46
+ "new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],",
47
+ "j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=",
48
+ "'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);",
49
+ "})(window,document,'script','dataLayer','{{containerId}}');"
51
50
  ].join("\n")
52
51
  }
53
52
  ]
54
53
  },
54
+ [GTM_INIT_DATA_TEMPLATE_ID]: {
55
+ items: [
56
+ // Initial data push - run when params change (e.g., user logs in)
57
+ {
58
+ body: "dataLayer.push({{json(initialData)}});"
59
+ }
60
+ ]
61
+ },
55
62
  [GTM_PAGEVIEW_TEMPLATE_ID]: {
56
63
  items: [
57
64
  {
@@ -122,6 +129,11 @@ function googleTagManagerBackend(opts) {
122
129
  if (event.clientContext) {
123
130
  return {
124
131
  items: [
132
+ {
133
+ type: "script-template",
134
+ templateId: GTM_INIT_DATA_TEMPLATE_ID,
135
+ params: { initialData }
136
+ },
125
137
  {
126
138
  type: "script-template",
127
139
  templateId: GTM_PAGEVIEW_TEMPLATE_ID,
@@ -137,6 +149,11 @@ function googleTagManagerBackend(opts) {
137
149
  templateId: GTM_INIT_TEMPLATE_ID,
138
150
  params: { containerId, initialData }
139
151
  },
152
+ {
153
+ type: "script-template",
154
+ templateId: GTM_INIT_DATA_TEMPLATE_ID,
155
+ params: { initialData }
156
+ },
140
157
  {
141
158
  type: "script-template",
142
159
  templateId: GTM_PAGEVIEW_TEMPLATE_ID,
@@ -21,10 +21,24 @@ __export(logging_exports, {
21
21
  loggingBackend: () => loggingBackend
22
22
  });
23
23
  module.exports = __toCommonJS(logging_exports);
24
+ const LOG_TEMPLATE_ID = "log-console";
24
25
  function loggingBackend() {
25
26
  return {
26
27
  name: "logging",
27
28
  supportsUpdates: true,
29
+ returnsClientActions: true,
30
+ getClientSideTemplates() {
31
+ return {
32
+ [LOG_TEMPLATE_ID]: {
33
+ deps: "{{eventId}}",
34
+ items: [
35
+ {
36
+ body: "console.log('[Nextlytics Log][client]', {{json(event)}});"
37
+ }
38
+ ]
39
+ }
40
+ };
41
+ },
28
42
  async onEvent(event) {
29
43
  const { type, eventId, serverContext, ...rest } = event;
30
44
  const method = serverContext?.method || "";
@@ -32,6 +46,25 @@ function loggingBackend() {
32
46
  const route = method && path ? `${method} ${path}` : "";
33
47
  console.log(`[Nextlytics Log] ${type}${route ? ` ${route}` : ""} (${eventId})`);
34
48
  console.log(JSON.stringify({ serverContext, ...rest }, null, 2));
49
+ if (event.origin === "client") {
50
+ return {
51
+ items: [
52
+ {
53
+ type: "script-template",
54
+ templateId: LOG_TEMPLATE_ID,
55
+ params: {
56
+ eventId: event.eventId,
57
+ event: {
58
+ type: event.type,
59
+ eventProps: event.properties,
60
+ userProps: event.userContext
61
+ }
62
+ }
63
+ }
64
+ ]
65
+ };
66
+ }
67
+ return void 0;
35
68
  },
36
69
  updateEvent(eventId, patch) {
37
70
  console.log(`[Nextlytics Log] Update ${eventId}`);
@@ -124,6 +124,7 @@ function segmentBackend(config) {
124
124
  } else {
125
125
  await send([{ type: "track", event: event.type, ...basePayload }]);
126
126
  }
127
+ return void 0;
127
128
  },
128
129
  updateEvent() {
129
130
  }
@@ -0,0 +1,35 @@
1
+ import * as react from 'react';
2
+
3
+ declare const debug: (...args: unknown[]) => void;
4
+ type NavigationEvent = {
5
+ softNavigation: boolean;
6
+ signal?: AbortSignal;
7
+ };
8
+ declare function usePathnameSafe(): string | null;
9
+ /**
10
+ * Hook that detects page navigations and calls the callback once per navigation.
11
+ *
12
+ * - App Router: initial load = hard nav, subsequent pathname changes = soft nav
13
+ * - Pages Router: every navigation changes requestId = always hard nav
14
+ *
15
+ * Provides an AbortSignal for soft navigations to cancel in-flight requests.
16
+ */
17
+ declare function useNavigation(requestId: string, onNavigate: (event: NavigationEvent) => void): void;
18
+ /** Props for the InjectScript utility component */
19
+ type InjectScriptProps = {
20
+ /** Inline script body (mutually exclusive with src) */
21
+ body?: string;
22
+ /** External script URL (mutually exclusive with body) */
23
+ src?: string;
24
+ /** Load external script async */
25
+ async?: boolean;
26
+ /** Dependencies that control re-injection. Empty/undefined = once, changes trigger re-injection */
27
+ deps?: unknown[];
28
+ };
29
+ /**
30
+ * Pure utility component that injects a script into the document head.
31
+ * Agnostic to templates, combining logic - just handles the injection lifecycle.
32
+ */
33
+ declare const InjectScript: react.NamedExoticComponent<InjectScriptProps>;
34
+
35
+ export { InjectScript, type InjectScriptProps, type NavigationEvent, debug, useNavigation, usePathnameSafe };
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var client_utils_exports = {};
21
+ __export(client_utils_exports, {
22
+ InjectScript: () => InjectScript,
23
+ debug: () => debug,
24
+ useNavigation: () => useNavigation,
25
+ usePathnameSafe: () => usePathnameSafe
26
+ });
27
+ module.exports = __toCommonJS(client_utils_exports);
28
+ var import_react = require("react");
29
+ const DEBUG_KEY = "nextlytics:debug";
30
+ function isDebugEnabled() {
31
+ if (typeof window === "undefined") return false;
32
+ try {
33
+ return localStorage.getItem(DEBUG_KEY) === "true";
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ const debug = (...args) => {
39
+ if (!isDebugEnabled()) return;
40
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].slice(0, -1);
41
+ console.log(`[${timestamp}] [Nextlytics]`, ...args);
42
+ };
43
+ let usePathnameImpl = null;
44
+ try {
45
+ usePathnameImpl = require("next/navigation").usePathname;
46
+ } catch {
47
+ }
48
+ function usePathnameSafe() {
49
+ if (!usePathnameImpl) return null;
50
+ return usePathnameImpl();
51
+ }
52
+ function useNavigation(requestId, onNavigate) {
53
+ const pathname = usePathnameSafe();
54
+ const stateRef = (0, import_react.useRef)(null);
55
+ const onNavigateRef = (0, import_react.useRef)(onNavigate);
56
+ onNavigateRef.current = onNavigate;
57
+ (0, import_react.useEffect)(() => {
58
+ const prev = stateRef.current;
59
+ const isInitial = prev === null;
60
+ const requestIdChanged = !isInitial && prev.requestId !== requestId;
61
+ const pathnameChanged = !isInitial && pathname !== null && prev.pathname !== pathname;
62
+ stateRef.current = { requestId, pathname };
63
+ if (!isInitial && !requestIdChanged && !pathnameChanged) {
64
+ return;
65
+ }
66
+ const softNavigation = !isInitial && !requestIdChanged && pathnameChanged;
67
+ debug("Navigation", {
68
+ isInitial,
69
+ softNavigation,
70
+ requestId,
71
+ pathname,
72
+ requestIdChanged,
73
+ pathnameChanged
74
+ });
75
+ if (softNavigation) {
76
+ const controller = new AbortController();
77
+ onNavigateRef.current({ softNavigation: true, signal: controller.signal });
78
+ return () => {
79
+ debug("Aborting previous soft navigation request");
80
+ controller.abort();
81
+ };
82
+ } else {
83
+ onNavigateRef.current({ softNavigation: false });
84
+ }
85
+ }, [requestId, pathname]);
86
+ }
87
+ function arraysEqual(a, b) {
88
+ if (a === b) return true;
89
+ if (!a || !b || a.length !== b.length) return false;
90
+ return a.every((v, i) => v === b[i]);
91
+ }
92
+ const InjectScript = (0, import_react.memo)(
93
+ function InjectScript2({ body, src, async: isAsync, deps = [] }) {
94
+ const depsKey = deps.map(String).join("\0");
95
+ (0, import_react.useEffect)(() => {
96
+ const el = document.createElement("script");
97
+ if (src) {
98
+ el.src = src;
99
+ if (isAsync) el.async = true;
100
+ debug("Inject external", { src, deps });
101
+ } else if (body) {
102
+ el.textContent = body;
103
+ debug("Inject inline", { body: body.slice(0, 100), deps });
104
+ }
105
+ document.head.appendChild(el);
106
+ return () => {
107
+ debug("Remove script", { deps });
108
+ el.remove();
109
+ };
110
+ }, [depsKey]);
111
+ return null;
112
+ },
113
+ (prev, next) => prev.body === next.body && prev.src === next.src && prev.async === next.async && arraysEqual(prev.deps, next.deps)
114
+ );
115
+ // Annotate the CommonJS export names for ESM import in node:
116
+ 0 && (module.exports = {
117
+ InjectScript,
118
+ debug,
119
+ useNavigation,
120
+ usePathnameSafe
121
+ });
package/dist/client.d.ts CHANGED
@@ -11,10 +11,7 @@ type NextlyticsContext = {
11
11
  templates?: Record<string, JavascriptTemplate>;
12
12
  };
13
13
  declare function NextlyticsClient(props: {
14
- ctx?: NextlyticsContext;
15
- requestId?: string;
16
- scripts?: TemplatizedScriptInsertion<unknown>[];
17
- templates?: Record<string, JavascriptTemplate>;
14
+ ctx: NextlyticsContext;
18
15
  children?: ReactNode;
19
16
  }): react_jsx_runtime.JSX.Element;
20
17
  type NextlyticsClientApi = {