@nextlytics/core 0.2.2 → 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.
- package/dist/api-handler.d.ts +11 -0
- package/dist/api-handler.js +182 -0
- package/dist/backends/ga.d.ts +5 -0
- package/dist/backends/ga.js +93 -17
- package/dist/backends/gtm.js +25 -8
- package/dist/backends/logging.js +33 -0
- package/dist/backends/segment.js +50 -17
- package/dist/client-utils.d.ts +35 -0
- package/dist/client-utils.js +121 -0
- package/dist/client.d.ts +1 -4
- package/dist/client.js +146 -67
- package/dist/config-helpers.d.ts +2 -2
- package/dist/config-helpers.js +3 -18
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -3
- package/dist/middleware.d.ts +2 -3
- package/dist/middleware.js +66 -127
- package/dist/pages-router.d.ts +6 -31
- package/dist/pages-router.js +1 -2
- package/dist/server-component-context.d.ts +10 -11
- package/dist/server-component-context.js +18 -28
- package/dist/server.d.ts +1 -6
- package/dist/server.js +59 -33
- package/dist/stable-hash.d.ts +6 -0
- package/dist/stable-hash.js +76 -0
- package/dist/types.d.ts +78 -17
- package/dist/uitils.d.ts +7 -1
- package/dist/uitils.js +39 -5
- package/package.json +1 -1
- package/dist/handlers.d.ts +0 -9
- package/dist/handlers.js +0 -123
|
@@ -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
|
+
});
|
package/dist/backends/ga.d.ts
CHANGED
|
@@ -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
|
|
package/dist/backends/ga.js
CHANGED
|
@@ -21,7 +21,9 @@ __export(ga_exports, {
|
|
|
21
21
|
googleAnalyticsBackend: () => googleAnalyticsBackend
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(ga_exports);
|
|
24
|
-
const
|
|
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
|
-
[
|
|
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
|
-
|
|
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(
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
190
|
+
initial_config.debug_mode = true;
|
|
164
191
|
}
|
|
192
|
+
const properties2 = {};
|
|
165
193
|
if (userId) {
|
|
166
|
-
|
|
194
|
+
properties2.user_id = userId;
|
|
167
195
|
}
|
|
168
196
|
if (userProperties) {
|
|
169
|
-
|
|
197
|
+
properties2.user_properties = userProperties;
|
|
170
198
|
}
|
|
171
199
|
return {
|
|
172
200
|
items: [
|
|
173
201
|
{
|
|
174
202
|
type: "script-template",
|
|
175
|
-
templateId:
|
|
203
|
+
templateId: GA_INIT_TEMPLATE,
|
|
176
204
|
params: {
|
|
177
205
|
measurementId,
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/backends/gtm.js
CHANGED
|
@@ -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
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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,
|
package/dist/backends/logging.js
CHANGED
|
@@ -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}`);
|
package/dist/backends/segment.js
CHANGED
|
@@ -37,6 +37,25 @@ function segmentBackend(config) {
|
|
|
37
37
|
throw new Error(`Segment error ${res.status}: ${text}`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
function buildUrl(event) {
|
|
41
|
+
if (event.clientContext?.url) return event.clientContext.url;
|
|
42
|
+
const { host: host2, path, search } = event.serverContext;
|
|
43
|
+
const protocol = host2.includes("localhost") || host2.match(/^[\d.:]+$/) ? "http" : "https";
|
|
44
|
+
const searchStr = Object.entries(search).flatMap(([k, vals]) => vals.map((v) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)).join("&");
|
|
45
|
+
return `${protocol}://${host2}${path}${searchStr ? `?${searchStr}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
function getSearchString(event) {
|
|
48
|
+
if (event.clientContext?.search) return event.clientContext.search;
|
|
49
|
+
return Object.entries(event.serverContext.search).flatMap(([k, vals]) => vals.map((v) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)).join("&");
|
|
50
|
+
}
|
|
51
|
+
function getReferringDomain(referer) {
|
|
52
|
+
if (!referer) return void 0;
|
|
53
|
+
try {
|
|
54
|
+
return new URL(referer).hostname;
|
|
55
|
+
} catch {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
40
59
|
function buildContext(event) {
|
|
41
60
|
const ctx = {
|
|
42
61
|
ip: event.serverContext.ip
|
|
@@ -44,32 +63,45 @@ function segmentBackend(config) {
|
|
|
44
63
|
if (event.userContext?.traits) {
|
|
45
64
|
ctx.traits = event.userContext.traits;
|
|
46
65
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
const cc = event.clientContext;
|
|
67
|
+
const sc = event.serverContext;
|
|
68
|
+
ctx.page = {
|
|
69
|
+
path: cc?.path ?? sc.path,
|
|
70
|
+
referrer: cc?.referer,
|
|
71
|
+
referring_domain: getReferringDomain(cc?.referer),
|
|
72
|
+
host: cc?.host ?? sc.host,
|
|
73
|
+
search: getSearchString(event),
|
|
74
|
+
title: cc?.title,
|
|
75
|
+
url: buildUrl(event)
|
|
76
|
+
};
|
|
77
|
+
if (cc) {
|
|
78
|
+
ctx.userAgent = cc.userAgent;
|
|
79
|
+
ctx.locale = cc.locale;
|
|
80
|
+
if (cc.screen) {
|
|
55
81
|
ctx.screen = {
|
|
56
|
-
width:
|
|
57
|
-
height:
|
|
58
|
-
innerWidth:
|
|
59
|
-
innerHeight:
|
|
60
|
-
density:
|
|
82
|
+
width: cc.screen.width,
|
|
83
|
+
height: cc.screen.height,
|
|
84
|
+
innerWidth: cc.screen.innerWidth,
|
|
85
|
+
innerHeight: cc.screen.innerHeight,
|
|
86
|
+
density: cc.screen.density
|
|
61
87
|
};
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
return ctx;
|
|
65
91
|
}
|
|
66
92
|
function buildProperties(event) {
|
|
93
|
+
const cc = event.clientContext;
|
|
94
|
+
const sc = event.serverContext;
|
|
67
95
|
return {
|
|
68
96
|
parentEventId: event.parentEventId,
|
|
69
|
-
path:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
path: cc?.path ?? sc.path,
|
|
98
|
+
url: buildUrl(event),
|
|
99
|
+
search: getSearchString(event),
|
|
100
|
+
hash: cc?.hash,
|
|
101
|
+
title: cc?.title,
|
|
102
|
+
referrer: cc?.referer,
|
|
103
|
+
width: cc?.screen?.innerWidth,
|
|
104
|
+
height: cc?.screen?.innerHeight,
|
|
73
105
|
...event.properties
|
|
74
106
|
};
|
|
75
107
|
}
|
|
@@ -92,6 +124,7 @@ function segmentBackend(config) {
|
|
|
92
124
|
} else {
|
|
93
125
|
await send([{ type: "track", event: event.type, ...basePayload }]);
|
|
94
126
|
}
|
|
127
|
+
return void 0;
|
|
95
128
|
},
|
|
96
129
|
updateEvent() {
|
|
97
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 };
|