@nextlytics/core 0.7.1 → 0.8.0-canary.120

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,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 };
@@ -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
+ });
@@ -11,7 +11,7 @@ interface ConfigValidationResult {
11
11
  valid: boolean;
12
12
  warnings: string[];
13
13
  }
14
- declare function validateConfig(_config: NextlyticsConfig): ConfigValidationResult;
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 };
@@ -40,8 +40,21 @@ function withDefaults(config) {
40
40
  }
41
41
  };
42
42
  }
43
- function validateConfig(_config) {
44
- return { valid: true, warnings: [] };
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) {
@@ -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,6 +79,47 @@ 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");
@@ -115,10 +157,10 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectT
115
157
  }
116
158
  const userContext = await (0, import_api_handler.getUserContext)(config, ctx);
117
159
  const extraProps = await (0, import_api_handler.getEventProps)(config, ctx, userContext);
118
- const pageViewEvent = createPageViewEvent(
160
+ const pageViewEvent = createEvent(
119
161
  pageRenderId,
120
162
  serverContext,
121
- isApiPath,
163
+ isApiPath ? "apiCall" : "pageView",
122
164
  userContext,
123
165
  anonId,
124
166
  extraProps
@@ -138,8 +180,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectT
138
180
  return response;
139
181
  };
140
182
  }
141
- function createPageViewEvent(pageRenderId, serverContext, isApiPath, userContext, anonymousUserId, extraProps) {
142
- const eventType = isApiPath ? "apiCall" : "pageView";
183
+ function createEvent(pageRenderId, serverContext, eventType, userContext, anonymousUserId, extraProps) {
143
184
  return {
144
185
  origin: "server",
145
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
- /** Skip tracking for specific paths */
181
+ /** @deprecated Use `capture` instead — return `false` for the paths you want to skip. */
152
182
  excludePaths?: (path: string) => boolean;
153
- /** Determine if path is API route. Default: () => false */
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextlytics/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0-canary.120",
4
4
  "description": "Analytics library for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {