@iorg1/trackking-next 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -117,6 +117,45 @@ server-side). Route templates support `:name` (one path segment) and `*` (the
117
117
  rest of the path). For full control, pass `resolveEvent: (req) => string | null`
118
118
  instead of `eventRoute` — return the event name, or `null` to pass through.
119
119
 
120
+ ### Conversions
121
+
122
+ A **conversion** is an event that gets attributed to the campaign the visitor
123
+ landed with. Page views already carry the landing query string, and the collector
124
+ turns it into a `campaign` label (`utm_campaign`, Google's auto-appended
125
+ `gad_campaignid`, a bare `gclid`, …). A conversion adds the request's client IP +
126
+ User-Agent; the collector hashes them into the same cookieless daily visitor id
127
+ the page view got, discards the IP, and looks up that visitor's landing campaign
128
+ for the day. Nothing is stored on the device, so a click and its conversion are
129
+ linked only when they happen on the same day.
130
+
131
+ Three ways to record one:
132
+
133
+ ```ts
134
+ // 1. From a route handler or server action, where the action actually completes.
135
+ // Use @iorg1/trackking-generic and pass the request headers:
136
+ import { headers } from "next/headers";
137
+ import { trackConversion } from "@iorg1/trackking-generic";
138
+ await trackConversion("report-ordered", { headers: await headers(), path: "/order" });
139
+
140
+ // 2. From your middleware — e.g. when the thank-you page is requested.
141
+ // (A page request repeats on reload, so prefer 1 when you can.)
142
+ import { track, trackConversion } from "@iorg1/trackking-next";
143
+ export function middleware(req: NextRequest, event: NextFetchEvent) {
144
+ if (req.nextUrl.pathname === "/order/confirmed") trackConversion(req, event, "order");
145
+ track(req, event);
146
+ return NextResponse.next();
147
+ }
148
+
149
+ // 3. From the browser, through the first-party event route:
150
+ navigator.sendBeacon("/data/signup?conversion=1");
151
+ // …or with a body: { conversion: true, props: { plan: "pro" } }
152
+ ```
153
+
154
+ If your code already knows the campaign (carried in a hidden form field, say),
155
+ pass `campaign: "…"` and the collector skips the lookup. Plain event beacons stay
156
+ identity-free unless you set `identifyEvents: true`, in which case they, too, are
157
+ attributed to the visitor's campaign.
158
+
120
159
  ## Options
121
160
 
122
161
  ```ts
@@ -128,6 +167,7 @@ createTrackingMiddleware({
128
167
  trustedProxyHops: 1, // trust the right x-forwarded-for entry (see below)
129
168
  eventRoute: "/data/:event", // first-party client-event route (see above)
130
169
  resolveEvent: (req) => …, // …or fully custom event extraction (overrides eventRoute)
170
+ identifyEvents: false, // attach visitor identity to plain event beacons too (see Conversions)
131
171
  eventEndpoint: process.env.TRACKKING_EVENT_ENDPOINT,
132
172
  debug: process.env.NODE_ENV !== "production", // verbose logging (see below)
133
173
  });
@@ -184,18 +224,27 @@ your server logs.
184
224
 
185
225
  ## What gets sent
186
226
 
187
- **Page views** — a single JSON object: `{ path, host, referrer, userAgent, ip,
188
- headers }`. The collector hashes `ip` into a daily-rotating visitor id **and
227
+ **Page views** — a single JSON object: `{ path, query, host, referrer, userAgent,
228
+ ip, headers }`. The collector hashes `ip` into a daily-rotating visitor id **and
189
229
  discards it** — the raw IP is never stored. The referrer is truncated to its
190
- host. `headers` is a small bag of low-entropy request headers (`Accept-Language`,
191
- `Accept`, the `Sec-Fetch-*` fetch-metadata set, and `Sec-CH-UA*` client hints)
192
- that help the collector tell real browsers from bots it is **not** a device
193
- fingerprint, and no cookies or client identifiers are involved.
230
+ host. `query` is the landing query string; the collector derives the campaign
231
+ label from it and stores it with secret-looking parameter values (`token`,
232
+ `code`, `email`, …) and, by default, click-id values (`gclid`, `fbclid`, …)
233
+ redacted. `headers` is a small bag of low-entropy request headers
234
+ (`Accept-Language`, `Accept`, the `Sec-Fetch-*` fetch-metadata set, and
235
+ `Sec-CH-UA*` client hints) that help the collector tell real browsers from bots —
236
+ it is **not** a device fingerprint, and no cookies or client identifiers are
237
+ involved.
194
238
 
195
239
  **Events** (if you use the event route) — `{ name, path, props }`, carrying no
196
240
  visitor identity. `props` is an optional small, flat bag of non-PII metadata,
197
241
  re-sanitized by the collector.
198
242
 
243
+ **Conversions** — the same, plus `conversion: true` and the request's `ip` +
244
+ `userAgent`, which the collector hashes into the daily visitor id (IP discarded)
245
+ to attribute the conversion to a campaign. Plain events carry identity only with
246
+ `identifyEvents: true`.
247
+
199
248
  No personal identifiers are persisted.
200
249
 
201
250
  ## License
package/dist/index.cjs CHANGED
@@ -27,7 +27,8 @@ __export(index_exports, {
27
27
  handleEvent: () => handleEvent,
28
28
  matchEventRoute: () => matchEventRoute,
29
29
  resolveEventName: () => resolveEventName,
30
- track: () => track
30
+ track: () => track,
31
+ trackConversion: () => trackConversion
31
32
  });
32
33
  module.exports = __toCommonJS(index_exports);
33
34
 
@@ -76,8 +77,13 @@ function parseEventBody(body) {
76
77
  if (b.props && typeof b.props === "object" && !Array.isArray(b.props)) {
77
78
  out.props = b.props;
78
79
  }
80
+ if (b.conversion === true) out.conversion = true;
81
+ if (typeof b.campaign === "string" && b.campaign.trim()) out.campaign = b.campaign.trim();
79
82
  return out;
80
83
  }
84
+ function isTruthyParam(value) {
85
+ return value !== null && value !== "" && value !== "0" && value.toLowerCase() !== "false";
86
+ }
81
87
 
82
88
  // src/middleware.ts
83
89
  function trustedProxyHops(config) {
@@ -136,6 +142,9 @@ function buildEvent(req, config = {}) {
136
142
  const ipFn = config.getClientIp ?? ((r) => getClientIp(r, trustedProxyHops(config)));
137
143
  return {
138
144
  path: req.nextUrl.pathname,
145
+ // The landing query string is what campaign attribution is derived from
146
+ // (utm_*, gclid, gad_*). The collector redacts secret-looking values.
147
+ query: req.nextUrl.search ? req.nextUrl.search.slice(1) : null,
139
148
  host: req.nextUrl.host || req.headers.get("host"),
140
149
  referrer: req.headers.get("referer"),
141
150
  userAgent: req.headers.get("user-agent"),
@@ -206,6 +215,10 @@ function resolveEventName(req, config = {}) {
206
215
  if (!template) return null;
207
216
  return matchEventRoute(req.nextUrl.pathname, template);
208
217
  }
218
+ function identityOf(req, config) {
219
+ const ipFn = config.getClientIp ?? ((r) => getClientIp(r, trustedProxyHops(config)));
220
+ return { ip: ipFn(req), userAgent: req.headers.get("user-agent") };
221
+ }
209
222
  async function sendClientEvent(payload, endpoint, apiKey, debug) {
210
223
  try {
211
224
  const res = await fetch(endpoint, {
@@ -227,7 +240,8 @@ async function handleEvent(req, event, config = {}) {
227
240
  if (req.method !== "GET" && req.method !== "HEAD") {
228
241
  body = await req.json().catch(() => null);
229
242
  }
230
- const { path, props } = parseEventBody(body);
243
+ const { path, props, conversion: bodyConversion, campaign } = parseEventBody(body);
244
+ const conversion = bodyConversion === true || isTruthyParam(req.nextUrl.searchParams.get("conversion"));
231
245
  const referrerPath = (() => {
232
246
  const ref = req.headers.get("referer");
233
247
  if (!ref) return void 0;
@@ -238,6 +252,9 @@ async function handleEvent(req, event, config = {}) {
238
252
  }
239
253
  })();
240
254
  const payload = { name, path: path ?? referrerPath, props };
255
+ if (conversion) payload.conversion = true;
256
+ if (campaign) payload.campaign = campaign;
257
+ if (conversion || config.identifyEvents) Object.assign(payload, identityOf(req, config));
241
258
  const endpoint = eventEndpointFor(config);
242
259
  const apiKey = config.apiKey ?? process.env.TRACKKING_API_KEY;
243
260
  if (endpoint && apiKey) {
@@ -248,6 +265,27 @@ async function handleEvent(req, event, config = {}) {
248
265
  }
249
266
  return import_server.NextResponse.json({});
250
267
  }
268
+ function trackConversion(req, event, name, options = {}) {
269
+ const { path, props, campaign, ...config } = options;
270
+ const endpoint = eventEndpointFor(config);
271
+ const apiKey = config.apiKey ?? process.env.TRACKKING_API_KEY;
272
+ const debug = isDebug(config);
273
+ const trimmed = name.trim();
274
+ if (!endpoint || !apiKey || !trimmed) {
275
+ if (debug) log("conversion skipped: not configured (endpoint/apiKey) or empty name");
276
+ return;
277
+ }
278
+ const payload = {
279
+ name: trimmed,
280
+ path: path ?? req.nextUrl.pathname,
281
+ props,
282
+ conversion: true,
283
+ ...identityOf(req, config)
284
+ };
285
+ if (campaign) payload.campaign = campaign;
286
+ if (debug) log("tracking conversion:", payload);
287
+ event.waitUntil(sendClientEvent(payload, endpoint, apiKey, debug));
288
+ }
251
289
  function createTrackingMiddleware(config = {}) {
252
290
  return async function middleware(req, event) {
253
291
  const eventResponse = await handleEvent(req, event, config);
@@ -265,5 +303,6 @@ function createTrackingMiddleware(config = {}) {
265
303
  handleEvent,
266
304
  matchEventRoute,
267
305
  resolveEventName,
268
- track
306
+ track,
307
+ trackConversion
269
308
  });
package/dist/index.d.cts CHANGED
@@ -9,6 +9,13 @@ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
9
9
  interface TrackkingEvent {
10
10
  /** Pathname only (no query string), e.g. "/en/pricing". */
11
11
  path: string;
12
+ /**
13
+ * Raw query string without the leading "?", or null. The collector derives the
14
+ * `campaign` label from it (utm_campaign, Google's gad_campaignid, a gclid, …)
15
+ * and stores it with secret-looking parameter values — and, by default,
16
+ * click-id values — redacted.
17
+ */
18
+ query?: string | null;
12
19
  /** Host of the tracked site, used by the collector to drop self-referrals. */
13
20
  host: string | null;
14
21
  /** Raw `Referer` header value, or null. Truncated to its host by the collector. */
@@ -67,6 +74,14 @@ interface TrackkingConfig {
67
74
  * untouched. Overrides `eventRoute`.
68
75
  */
69
76
  resolveEvent?: (req: NextRequest) => string | null;
77
+ /**
78
+ * Attach the visitor identity — the request's client IP and User-Agent, which
79
+ * the collector hashes into the same daily visitor id page views get and then
80
+ * discards — to *every* first-party event beacon, so plain events can be
81
+ * attributed to the campaign the visitor landed with. Default false: events
82
+ * stay identity-free unless they are conversions, which always carry it.
83
+ */
84
+ identifyEvents?: boolean;
70
85
  /** Override how the client IP is extracted from the request. */
71
86
  getClientIp?: (req: NextRequest) => string | null;
72
87
  /**
@@ -86,6 +101,19 @@ interface TrackkingConfig {
86
101
  */
87
102
  debug?: boolean;
88
103
  }
104
+ /** What a conversion records besides its name. */
105
+ interface ConversionOptions {
106
+ /** The route the conversion relates to. Defaults to the request's pathname. */
107
+ path?: string;
108
+ /** Small, flat bag of non-PII metadata (re-sanitized by the collector). */
109
+ props?: Record<string, string | number | boolean>;
110
+ /**
111
+ * Campaign label to record instead of letting the collector attribute the
112
+ * conversion from the visitor's landing page view — for a flow that carried
113
+ * the campaign itself, e.g. in a hidden form field.
114
+ */
115
+ campaign?: string;
116
+ }
89
117
 
90
118
  /**
91
119
  * Best-effort client IP. Behind a proxy (Vercel, Caddy, nginx) the real visitor
@@ -123,6 +151,12 @@ declare function resolveEventName(req: NextRequest, config?: TrackkingConfig): s
123
151
  * null when the request is not an event beacon — the caller should then proceed
124
152
  * with normal page-view tracking.
125
153
  *
154
+ * A beacon becomes a *conversion* when its body says `{ conversion: true }` or
155
+ * the URL carries `?conversion=1`. Conversions always travel with the visitor
156
+ * identity (client IP + User-Agent, hashed and discarded by the collector) so
157
+ * they can be attributed to the campaign the visitor landed with; plain events
158
+ * only do when `identifyEvents` is set.
159
+ *
126
160
  * export async function middleware(req, event) {
127
161
  * const res = await handleEvent(req, event, config);
128
162
  * if (res) return res; // it was a /data/<event> beacon
@@ -131,6 +165,25 @@ declare function resolveEventName(req: NextRequest, config?: TrackkingConfig): s
131
165
  * }
132
166
  */
133
167
  declare function handleEvent(req: NextRequest, event: NextFetchEvent, config?: TrackkingConfig): Promise<NextResponse | null>;
168
+ /**
169
+ * Record a conversion for the current visitor from inside your middleware — for
170
+ * example when the "thank you" page is requested. The collector attributes it
171
+ * to the campaign the visitor landed with today (last click) through the same
172
+ * cookieless daily hash page views use; nothing is stored on the device.
173
+ * Fire-and-forget; never throws.
174
+ *
175
+ * export function middleware(req: NextRequest, event: NextFetchEvent) {
176
+ * if (req.nextUrl.pathname === "/order/confirmed") trackConversion(req, event, "order");
177
+ * track(req, event);
178
+ * return NextResponse.next();
179
+ * }
180
+ *
181
+ * A page request is repeatable (reloads, back button), so prefer recording
182
+ * conversions where the action actually completes — a route handler or server
183
+ * action — with `trackConversion` from @iorg1/trackking-generic, passing the
184
+ * request headers. This middleware variant is for when that isn't possible.
185
+ */
186
+ declare function trackConversion(req: NextRequest, event: NextFetchEvent, name: string, options?: ConversionOptions & TrackkingConfig): void;
134
187
  /**
135
188
  * Returns a complete Next.js middleware function for the common case where
136
189
  * tracking is the only thing your middleware does. It also serves the
@@ -155,4 +208,4 @@ declare function matchEventRoute(pathname: string, template: string): string | n
155
208
  */
156
209
  declare function deriveEventEndpoint(base: string | undefined): string | undefined;
157
210
 
158
- export { type TrackkingConfig, type TrackkingEvent, buildEvent, createTrackingMiddleware, deriveEventEndpoint, getClientIp, handleEvent, matchEventRoute, resolveEventName, track };
211
+ export { type ConversionOptions, type TrackkingConfig, type TrackkingEvent, buildEvent, createTrackingMiddleware, deriveEventEndpoint, getClientIp, handleEvent, matchEventRoute, resolveEventName, track, trackConversion };
package/dist/index.d.ts CHANGED
@@ -9,6 +9,13 @@ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
9
9
  interface TrackkingEvent {
10
10
  /** Pathname only (no query string), e.g. "/en/pricing". */
11
11
  path: string;
12
+ /**
13
+ * Raw query string without the leading "?", or null. The collector derives the
14
+ * `campaign` label from it (utm_campaign, Google's gad_campaignid, a gclid, …)
15
+ * and stores it with secret-looking parameter values — and, by default,
16
+ * click-id values — redacted.
17
+ */
18
+ query?: string | null;
12
19
  /** Host of the tracked site, used by the collector to drop self-referrals. */
13
20
  host: string | null;
14
21
  /** Raw `Referer` header value, or null. Truncated to its host by the collector. */
@@ -67,6 +74,14 @@ interface TrackkingConfig {
67
74
  * untouched. Overrides `eventRoute`.
68
75
  */
69
76
  resolveEvent?: (req: NextRequest) => string | null;
77
+ /**
78
+ * Attach the visitor identity — the request's client IP and User-Agent, which
79
+ * the collector hashes into the same daily visitor id page views get and then
80
+ * discards — to *every* first-party event beacon, so plain events can be
81
+ * attributed to the campaign the visitor landed with. Default false: events
82
+ * stay identity-free unless they are conversions, which always carry it.
83
+ */
84
+ identifyEvents?: boolean;
70
85
  /** Override how the client IP is extracted from the request. */
71
86
  getClientIp?: (req: NextRequest) => string | null;
72
87
  /**
@@ -86,6 +101,19 @@ interface TrackkingConfig {
86
101
  */
87
102
  debug?: boolean;
88
103
  }
104
+ /** What a conversion records besides its name. */
105
+ interface ConversionOptions {
106
+ /** The route the conversion relates to. Defaults to the request's pathname. */
107
+ path?: string;
108
+ /** Small, flat bag of non-PII metadata (re-sanitized by the collector). */
109
+ props?: Record<string, string | number | boolean>;
110
+ /**
111
+ * Campaign label to record instead of letting the collector attribute the
112
+ * conversion from the visitor's landing page view — for a flow that carried
113
+ * the campaign itself, e.g. in a hidden form field.
114
+ */
115
+ campaign?: string;
116
+ }
89
117
 
90
118
  /**
91
119
  * Best-effort client IP. Behind a proxy (Vercel, Caddy, nginx) the real visitor
@@ -123,6 +151,12 @@ declare function resolveEventName(req: NextRequest, config?: TrackkingConfig): s
123
151
  * null when the request is not an event beacon — the caller should then proceed
124
152
  * with normal page-view tracking.
125
153
  *
154
+ * A beacon becomes a *conversion* when its body says `{ conversion: true }` or
155
+ * the URL carries `?conversion=1`. Conversions always travel with the visitor
156
+ * identity (client IP + User-Agent, hashed and discarded by the collector) so
157
+ * they can be attributed to the campaign the visitor landed with; plain events
158
+ * only do when `identifyEvents` is set.
159
+ *
126
160
  * export async function middleware(req, event) {
127
161
  * const res = await handleEvent(req, event, config);
128
162
  * if (res) return res; // it was a /data/<event> beacon
@@ -131,6 +165,25 @@ declare function resolveEventName(req: NextRequest, config?: TrackkingConfig): s
131
165
  * }
132
166
  */
133
167
  declare function handleEvent(req: NextRequest, event: NextFetchEvent, config?: TrackkingConfig): Promise<NextResponse | null>;
168
+ /**
169
+ * Record a conversion for the current visitor from inside your middleware — for
170
+ * example when the "thank you" page is requested. The collector attributes it
171
+ * to the campaign the visitor landed with today (last click) through the same
172
+ * cookieless daily hash page views use; nothing is stored on the device.
173
+ * Fire-and-forget; never throws.
174
+ *
175
+ * export function middleware(req: NextRequest, event: NextFetchEvent) {
176
+ * if (req.nextUrl.pathname === "/order/confirmed") trackConversion(req, event, "order");
177
+ * track(req, event);
178
+ * return NextResponse.next();
179
+ * }
180
+ *
181
+ * A page request is repeatable (reloads, back button), so prefer recording
182
+ * conversions where the action actually completes — a route handler or server
183
+ * action — with `trackConversion` from @iorg1/trackking-generic, passing the
184
+ * request headers. This middleware variant is for when that isn't possible.
185
+ */
186
+ declare function trackConversion(req: NextRequest, event: NextFetchEvent, name: string, options?: ConversionOptions & TrackkingConfig): void;
134
187
  /**
135
188
  * Returns a complete Next.js middleware function for the common case where
136
189
  * tracking is the only thing your middleware does. It also serves the
@@ -155,4 +208,4 @@ declare function matchEventRoute(pathname: string, template: string): string | n
155
208
  */
156
209
  declare function deriveEventEndpoint(base: string | undefined): string | undefined;
157
210
 
158
- export { type TrackkingConfig, type TrackkingEvent, buildEvent, createTrackingMiddleware, deriveEventEndpoint, getClientIp, handleEvent, matchEventRoute, resolveEventName, track };
211
+ export { type ConversionOptions, type TrackkingConfig, type TrackkingEvent, buildEvent, createTrackingMiddleware, deriveEventEndpoint, getClientIp, handleEvent, matchEventRoute, resolveEventName, track, trackConversion };
package/dist/index.js CHANGED
@@ -43,8 +43,13 @@ function parseEventBody(body) {
43
43
  if (b.props && typeof b.props === "object" && !Array.isArray(b.props)) {
44
44
  out.props = b.props;
45
45
  }
46
+ if (b.conversion === true) out.conversion = true;
47
+ if (typeof b.campaign === "string" && b.campaign.trim()) out.campaign = b.campaign.trim();
46
48
  return out;
47
49
  }
50
+ function isTruthyParam(value) {
51
+ return value !== null && value !== "" && value !== "0" && value.toLowerCase() !== "false";
52
+ }
48
53
 
49
54
  // src/middleware.ts
50
55
  function trustedProxyHops(config) {
@@ -103,6 +108,9 @@ function buildEvent(req, config = {}) {
103
108
  const ipFn = config.getClientIp ?? ((r) => getClientIp(r, trustedProxyHops(config)));
104
109
  return {
105
110
  path: req.nextUrl.pathname,
111
+ // The landing query string is what campaign attribution is derived from
112
+ // (utm_*, gclid, gad_*). The collector redacts secret-looking values.
113
+ query: req.nextUrl.search ? req.nextUrl.search.slice(1) : null,
106
114
  host: req.nextUrl.host || req.headers.get("host"),
107
115
  referrer: req.headers.get("referer"),
108
116
  userAgent: req.headers.get("user-agent"),
@@ -173,6 +181,10 @@ function resolveEventName(req, config = {}) {
173
181
  if (!template) return null;
174
182
  return matchEventRoute(req.nextUrl.pathname, template);
175
183
  }
184
+ function identityOf(req, config) {
185
+ const ipFn = config.getClientIp ?? ((r) => getClientIp(r, trustedProxyHops(config)));
186
+ return { ip: ipFn(req), userAgent: req.headers.get("user-agent") };
187
+ }
176
188
  async function sendClientEvent(payload, endpoint, apiKey, debug) {
177
189
  try {
178
190
  const res = await fetch(endpoint, {
@@ -194,7 +206,8 @@ async function handleEvent(req, event, config = {}) {
194
206
  if (req.method !== "GET" && req.method !== "HEAD") {
195
207
  body = await req.json().catch(() => null);
196
208
  }
197
- const { path, props } = parseEventBody(body);
209
+ const { path, props, conversion: bodyConversion, campaign } = parseEventBody(body);
210
+ const conversion = bodyConversion === true || isTruthyParam(req.nextUrl.searchParams.get("conversion"));
198
211
  const referrerPath = (() => {
199
212
  const ref = req.headers.get("referer");
200
213
  if (!ref) return void 0;
@@ -205,6 +218,9 @@ async function handleEvent(req, event, config = {}) {
205
218
  }
206
219
  })();
207
220
  const payload = { name, path: path ?? referrerPath, props };
221
+ if (conversion) payload.conversion = true;
222
+ if (campaign) payload.campaign = campaign;
223
+ if (conversion || config.identifyEvents) Object.assign(payload, identityOf(req, config));
208
224
  const endpoint = eventEndpointFor(config);
209
225
  const apiKey = config.apiKey ?? process.env.TRACKKING_API_KEY;
210
226
  if (endpoint && apiKey) {
@@ -215,6 +231,27 @@ async function handleEvent(req, event, config = {}) {
215
231
  }
216
232
  return NextResponse.json({});
217
233
  }
234
+ function trackConversion(req, event, name, options = {}) {
235
+ const { path, props, campaign, ...config } = options;
236
+ const endpoint = eventEndpointFor(config);
237
+ const apiKey = config.apiKey ?? process.env.TRACKKING_API_KEY;
238
+ const debug = isDebug(config);
239
+ const trimmed = name.trim();
240
+ if (!endpoint || !apiKey || !trimmed) {
241
+ if (debug) log("conversion skipped: not configured (endpoint/apiKey) or empty name");
242
+ return;
243
+ }
244
+ const payload = {
245
+ name: trimmed,
246
+ path: path ?? req.nextUrl.pathname,
247
+ props,
248
+ conversion: true,
249
+ ...identityOf(req, config)
250
+ };
251
+ if (campaign) payload.campaign = campaign;
252
+ if (debug) log("tracking conversion:", payload);
253
+ event.waitUntil(sendClientEvent(payload, endpoint, apiKey, debug));
254
+ }
218
255
  function createTrackingMiddleware(config = {}) {
219
256
  return async function middleware(req, event) {
220
257
  const eventResponse = await handleEvent(req, event, config);
@@ -231,5 +268,6 @@ export {
231
268
  handleEvent,
232
269
  matchEventRoute,
233
270
  resolveEventName,
234
- track
271
+ track,
272
+ trackConversion
235
273
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@iorg1/trackking-next",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/iorg1/trackking.git",
7
7
  "directory": "packages/middleware"
8
8
  },
9
- "description": "Cookieless, server-side page-view tracking for Next.js. Drop-in middleware that beacons visits to a Trackking collector no cookies, no client-side script.",
9
+ "description": "Cookieless, server-side page-view tracking for Next.js. Drop-in middleware that beacons visits to a Trackking collector \u2014 no cookies, no client-side script.",
10
10
  "license": "MIT",
11
11
  "type": "module",
12
12
  "main": "./dist/index.cjs",