@nextlytics/core 0.7.0 → 0.7.1-canary.119
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/capture.d.ts +33 -0
- package/dist/capture.js +39 -0
- package/dist/config-helpers.d.ts +1 -1
- package/dist/config-helpers.js +15 -2
- package/dist/middleware.js +46 -6
- package/dist/types.d.ts +34 -4
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CaptureRequest } from './types.js';
|
|
2
|
+
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
|
+
import 'next/server';
|
|
4
|
+
import 'next';
|
|
5
|
+
|
|
6
|
+
/** The mechanical request facts the capture decision needs. */
|
|
7
|
+
type CaptureReqInfo = {
|
|
8
|
+
/** Browser-initiated sub-request (RSC/XHR/fetch/subresource). */
|
|
9
|
+
isBrowserSubrequest: boolean;
|
|
10
|
+
/** Hard document navigation (Sec-Fetch-Dest: document / mode: navigate). */
|
|
11
|
+
isDocumentRequest: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the event type for a request under `capture` configuration, or `null`
|
|
15
|
+
* to skip. Pure and side-effect free so it can be unit-tested directly.
|
|
16
|
+
*
|
|
17
|
+
* Mechanical noise is filtered first regardless of what `capture` returns:
|
|
18
|
+
* - browser sub-requests (RSC soft-nav / XHR / fetch / subresource) — would
|
|
19
|
+
* duplicate the client-side pageView, so never recorded here;
|
|
20
|
+
* - non-GET requests that aren't document navigations (HEAD probes, webhook
|
|
21
|
+
* POSTs, etc.) — not page views.
|
|
22
|
+
*
|
|
23
|
+
* Everything else — a real browser navigation, or a direct GET from a
|
|
24
|
+
* non-browser client — is handed to `capture`, whose return decides:
|
|
25
|
+
* `false` → skip, `true` → "pageView", `"<type>"` → that event type.
|
|
26
|
+
*/
|
|
27
|
+
declare function resolveCaptureType(capture: (req: CaptureRequest) => boolean | string, reqInfo: CaptureReqInfo, req: {
|
|
28
|
+
path: string;
|
|
29
|
+
method: string;
|
|
30
|
+
userAgent?: string;
|
|
31
|
+
}): string | null;
|
|
32
|
+
|
|
33
|
+
export { resolveCaptureType };
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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 capture_exports = {};
|
|
20
|
+
__export(capture_exports, {
|
|
21
|
+
resolveCaptureType: () => resolveCaptureType
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(capture_exports);
|
|
24
|
+
function resolveCaptureType(capture, reqInfo, req) {
|
|
25
|
+
if (reqInfo.isBrowserSubrequest) return null;
|
|
26
|
+
if (req.method !== "GET" && !reqInfo.isDocumentRequest) return null;
|
|
27
|
+
const result = capture({
|
|
28
|
+
path: req.path,
|
|
29
|
+
method: req.method,
|
|
30
|
+
fromBrowser: reqInfo.isDocumentRequest,
|
|
31
|
+
userAgent: req.userAgent
|
|
32
|
+
});
|
|
33
|
+
if (result === false) return null;
|
|
34
|
+
return result === true ? "pageView" : result;
|
|
35
|
+
}
|
|
36
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
37
|
+
0 && (module.exports = {
|
|
38
|
+
resolveCaptureType
|
|
39
|
+
});
|
package/dist/config-helpers.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface ConfigValidationResult {
|
|
|
11
11
|
valid: boolean;
|
|
12
12
|
warnings: string[];
|
|
13
13
|
}
|
|
14
|
-
declare function validateConfig(
|
|
14
|
+
declare function validateConfig(config: NextlyticsConfig): ConfigValidationResult;
|
|
15
15
|
declare function logConfigWarnings(result: ConfigValidationResult): void;
|
|
16
16
|
|
|
17
17
|
export { type ConfigValidationResult, type NextlyticsConfigWithDefaults, logConfigWarnings, validateConfig, withDefaults };
|
package/dist/config-helpers.js
CHANGED
|
@@ -40,8 +40,21 @@ function withDefaults(config) {
|
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
function validateConfig(
|
|
44
|
-
|
|
43
|
+
function validateConfig(config) {
|
|
44
|
+
const warnings = [];
|
|
45
|
+
const deprecated = ["isApiPath", "excludeApiCalls", "excludePaths"].filter(
|
|
46
|
+
(k) => config[k] !== void 0
|
|
47
|
+
);
|
|
48
|
+
if (config.capture && deprecated.length > 0) {
|
|
49
|
+
warnings.push(
|
|
50
|
+
`[Nextlytics] \`capture\` is set, so the deprecated option(s) ${deprecated.join(", ")} are ignored. Move that logic into \`capture\`.`
|
|
51
|
+
);
|
|
52
|
+
} else if (deprecated.length > 0) {
|
|
53
|
+
warnings.push(
|
|
54
|
+
`[Nextlytics] ${deprecated.join(", ")} are deprecated; prefer \`capture\` (return false / true / "<eventType>").`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return { valid: true, warnings };
|
|
45
58
|
}
|
|
46
59
|
function logConfigWarnings(result) {
|
|
47
60
|
for (const warning of result.warnings) {
|
package/dist/middleware.js
CHANGED
|
@@ -24,6 +24,7 @@ module.exports = __toCommonJS(middleware_exports);
|
|
|
24
24
|
var import_server = require("next/server");
|
|
25
25
|
var import_server_component_context = require("./server-component-context");
|
|
26
26
|
var import_uitils = require("./uitils");
|
|
27
|
+
var import_capture = require("./capture");
|
|
27
28
|
var import_anonymous_user = require("./anonymous-user");
|
|
28
29
|
var import_api_handler = require("./api-handler");
|
|
29
30
|
function createRequestContext(request) {
|
|
@@ -78,13 +79,53 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectT
|
|
|
78
79
|
response2.headers.set(import_server_component_context.headerNames.active, "1");
|
|
79
80
|
return response2;
|
|
80
81
|
}
|
|
82
|
+
if (config.capture) {
|
|
83
|
+
const eventType = (0, import_capture.resolveCaptureType)(config.capture, reqInfo, {
|
|
84
|
+
path: pathname,
|
|
85
|
+
method: request.method,
|
|
86
|
+
userAgent: request.headers.get("user-agent") ?? void 0
|
|
87
|
+
});
|
|
88
|
+
if (eventType === null) {
|
|
89
|
+
const response3 = import_server.NextResponse.next();
|
|
90
|
+
response3.headers.set(import_server_component_context.headerNames.active, "1");
|
|
91
|
+
return response3;
|
|
92
|
+
}
|
|
93
|
+
const pageRenderId2 = (0, import_uitils.generateId)();
|
|
94
|
+
const serverContext2 = (0, import_uitils.createServerContext)(request);
|
|
95
|
+
const response2 = import_server.NextResponse.next();
|
|
96
|
+
const ctx2 = createRequestContext(request);
|
|
97
|
+
response2.cookies.set(import_server_component_context.LAST_PAGE_RENDER_ID_COOKIE, pageRenderId2, { path: "/" });
|
|
98
|
+
const { anonId: anonId2 } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx: ctx2, serverContext: serverContext2, config, response: response2 });
|
|
99
|
+
const userContext2 = await (0, import_api_handler.getUserContext)(config, ctx2);
|
|
100
|
+
const extraProps2 = await (0, import_api_handler.getEventProps)(config, ctx2, userContext2);
|
|
101
|
+
const event = createEvent(
|
|
102
|
+
pageRenderId2,
|
|
103
|
+
serverContext2,
|
|
104
|
+
eventType,
|
|
105
|
+
userContext2,
|
|
106
|
+
anonId2,
|
|
107
|
+
extraProps2
|
|
108
|
+
);
|
|
109
|
+
const { clientActions: clientActions2, completion: completion2 } = dispatchEvent(event, ctx2, "on-request");
|
|
110
|
+
const actions2 = await clientActions2;
|
|
111
|
+
const scripts2 = actions2.items.filter(
|
|
112
|
+
(i) => i.type === "script-template"
|
|
113
|
+
);
|
|
114
|
+
(0, import_server.after)(() => completion2);
|
|
115
|
+
(0, import_server_component_context.serializeServerComponentContext)(response2, {
|
|
116
|
+
pageRenderId: pageRenderId2,
|
|
117
|
+
pathname: request.nextUrl.pathname,
|
|
118
|
+
search: request.nextUrl.search,
|
|
119
|
+
scripts: scripts2
|
|
120
|
+
});
|
|
121
|
+
return response2;
|
|
122
|
+
}
|
|
81
123
|
if (reqInfo.isBrowserSubrequest && !config.isApiPath(pathname)) {
|
|
82
124
|
const response2 = import_server.NextResponse.next();
|
|
83
125
|
response2.headers.set(import_server_component_context.headerNames.active, "1");
|
|
84
126
|
return response2;
|
|
85
127
|
}
|
|
86
|
-
|
|
87
|
-
if (!isReadMethod && !reqInfo.isDocumentRequest && !config.isApiPath(pathname)) {
|
|
128
|
+
if (request.method !== "GET" && !reqInfo.isDocumentRequest && !config.isApiPath(pathname)) {
|
|
88
129
|
const response2 = import_server.NextResponse.next();
|
|
89
130
|
response2.headers.set(import_server_component_context.headerNames.active, "1");
|
|
90
131
|
return response2;
|
|
@@ -116,10 +157,10 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectT
|
|
|
116
157
|
}
|
|
117
158
|
const userContext = await (0, import_api_handler.getUserContext)(config, ctx);
|
|
118
159
|
const extraProps = await (0, import_api_handler.getEventProps)(config, ctx, userContext);
|
|
119
|
-
const pageViewEvent =
|
|
160
|
+
const pageViewEvent = createEvent(
|
|
120
161
|
pageRenderId,
|
|
121
162
|
serverContext,
|
|
122
|
-
isApiPath,
|
|
163
|
+
isApiPath ? "apiCall" : "pageView",
|
|
123
164
|
userContext,
|
|
124
165
|
anonId,
|
|
125
166
|
extraProps
|
|
@@ -139,8 +180,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectT
|
|
|
139
180
|
return response;
|
|
140
181
|
};
|
|
141
182
|
}
|
|
142
|
-
function
|
|
143
|
-
const eventType = isApiPath ? "apiCall" : "pageView";
|
|
183
|
+
function createEvent(pageRenderId, serverContext, eventType, userContext, anonymousUserId, extraProps) {
|
|
144
184
|
return {
|
|
145
185
|
origin: "server",
|
|
146
186
|
collectedAt: serverContext.collectedAt.toISOString(),
|
package/dist/types.d.ts
CHANGED
|
@@ -131,9 +131,39 @@ type BackendWithConfig = {
|
|
|
131
131
|
};
|
|
132
132
|
/** Backend config entry - either a backend directly or with config */
|
|
133
133
|
type BackendConfigEntry = NextlyticsBackend | NextlyticsBackendFactory | BackendWithConfig;
|
|
134
|
+
/** The request `capture` decides on. Only real browser navigations and direct
|
|
135
|
+
* (non-browser) requests reach `capture`; RSC/XHR sub-requests, prefetches,
|
|
136
|
+
* static assets, and non-GET non-navigation writes are skipped before it. */
|
|
137
|
+
type CaptureRequest = {
|
|
138
|
+
/** URL pathname, e.g. "/docs/quick-start.md" */
|
|
139
|
+
path: string;
|
|
140
|
+
/** HTTP method (GET, POST, …) */
|
|
141
|
+
method: string;
|
|
142
|
+
/** True when a real browser navigated here (Sec-Fetch-Dest: document). False
|
|
143
|
+
* for programmatic clients — agents, crawlers, curl, server-to-server — which
|
|
144
|
+
* omit Sec-Fetch-* headers. */
|
|
145
|
+
fromBrowser: boolean;
|
|
146
|
+
/** User-Agent header, if present. */
|
|
147
|
+
userAgent?: string;
|
|
148
|
+
};
|
|
134
149
|
type NextlyticsConfig = {
|
|
135
150
|
/** Enable debug logging (shows backend stats for each event) */
|
|
136
151
|
debug?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Decide whether — and as what event type — to record a request.
|
|
154
|
+
*
|
|
155
|
+
* - `false` → don't record
|
|
156
|
+
* - `true` → record as the default type ("pageView")
|
|
157
|
+
* - `"<type>"` → record with this string as `event.type` (e.g. "apiCall")
|
|
158
|
+
*
|
|
159
|
+
* Real browser users are the common case; programmatic clients (agents,
|
|
160
|
+
* crawlers, curl) are identified by `fromBrowser: false`. Defaults to
|
|
161
|
+
* `({ fromBrowser }) => fromBrowser` — i.e. track browser navigations only.
|
|
162
|
+
*
|
|
163
|
+
* When set, this is the single source of truth and the deprecated
|
|
164
|
+
* `isApiPath` / `excludeApiCalls` / `excludePaths` options are ignored.
|
|
165
|
+
*/
|
|
166
|
+
capture?: (req: CaptureRequest) => boolean | string;
|
|
137
167
|
anonymousUsers?: {
|
|
138
168
|
/** Store anonymous ID in cookies */
|
|
139
169
|
useCookies?: boolean;
|
|
@@ -146,11 +176,11 @@ type NextlyticsConfig = {
|
|
|
146
176
|
/** Cookie max age in seconds (default: 2 years) */
|
|
147
177
|
cookieMaxAge?: number;
|
|
148
178
|
};
|
|
149
|
-
/** Skip tracking for API routes */
|
|
179
|
+
/** @deprecated Use `capture` instead — return `false` for API paths. Skip tracking for API routes. */
|
|
150
180
|
excludeApiCalls?: boolean;
|
|
151
|
-
/**
|
|
181
|
+
/** @deprecated Use `capture` instead — return `false` for the paths you want to skip. */
|
|
152
182
|
excludePaths?: (path: string) => boolean;
|
|
153
|
-
/**
|
|
183
|
+
/** @deprecated Use `capture` instead — return `"apiCall"` (or `false`) for API paths. */
|
|
154
184
|
isApiPath?: (path: string) => boolean;
|
|
155
185
|
/** Endpoint for client events. Default: "/api/event" */
|
|
156
186
|
eventEndpoint?: string;
|
|
@@ -298,4 +328,4 @@ type NextlyticsResult = {
|
|
|
298
328
|
}) => Promise<React.ReactElement>;
|
|
299
329
|
};
|
|
300
330
|
|
|
301
|
-
export type { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, ClientAction, ClientActionItem, ClientContext, ClientRequest, ClientRequestResult, DispatchResult, JavascriptTemplate, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsClientContext, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, PageViewDelivery, PagesRouterContext, RequestContext, ScriptElement, ServerEventContext, TemplatizedScriptInsertion, UserContext };
|
|
331
|
+
export type { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, CaptureRequest, ClientAction, ClientActionItem, ClientContext, ClientRequest, ClientRequestResult, DispatchResult, JavascriptTemplate, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsClientContext, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, PageViewDelivery, PagesRouterContext, RequestContext, ScriptElement, ServerEventContext, TemplatizedScriptInsertion, UserContext };
|