@pulsekit/next 0.0.1
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/client.d.mts +7 -0
- package/dist/client.mjs +99 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.mjs +92 -0
- package/package.json +41 -0
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/PulseTracker.tsx
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
function getSessionId() {
|
|
5
|
+
const key = "pulse_session_id";
|
|
6
|
+
let id = sessionStorage.getItem(key);
|
|
7
|
+
if (!id) {
|
|
8
|
+
id = crypto.randomUUID();
|
|
9
|
+
sessionStorage.setItem(key, id);
|
|
10
|
+
}
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
function PulseTracker({ endpoint = "/api/pulse", excludePaths }) {
|
|
14
|
+
const vitalsRef = useRef({});
|
|
15
|
+
const hasSentVitalsRef = useRef(false);
|
|
16
|
+
const sessionIdRef = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
sessionIdRef.current = getSessionId();
|
|
19
|
+
}, []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
22
|
+
document.cookie = `pulse_tz=${encodeURIComponent(tz)}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
|
23
|
+
if (excludePaths?.includes(window.location.pathname)) return;
|
|
24
|
+
const sessionId = getSessionId();
|
|
25
|
+
sessionIdRef.current = sessionId;
|
|
26
|
+
fetch(endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
type: "pageview",
|
|
31
|
+
path: window.location.pathname,
|
|
32
|
+
sessionId
|
|
33
|
+
})
|
|
34
|
+
}).catch(() => {
|
|
35
|
+
});
|
|
36
|
+
}, [endpoint, excludePaths]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (excludePaths?.includes(window.location.pathname)) return;
|
|
39
|
+
import("web-vitals").then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => {
|
|
40
|
+
onLCP((m) => {
|
|
41
|
+
vitalsRef.current.lcp = m.value;
|
|
42
|
+
});
|
|
43
|
+
onINP((m) => {
|
|
44
|
+
vitalsRef.current.inp = m.value;
|
|
45
|
+
}, { reportAllChanges: true });
|
|
46
|
+
onCLS((m) => {
|
|
47
|
+
vitalsRef.current.cls = m.value;
|
|
48
|
+
}, { reportAllChanges: true });
|
|
49
|
+
onFCP((m) => {
|
|
50
|
+
vitalsRef.current.fcp = m.value;
|
|
51
|
+
});
|
|
52
|
+
onTTFB((m) => {
|
|
53
|
+
vitalsRef.current.ttfb = m.value;
|
|
54
|
+
});
|
|
55
|
+
}).catch(() => {
|
|
56
|
+
});
|
|
57
|
+
function sendVitals() {
|
|
58
|
+
if (hasSentVitalsRef.current) return;
|
|
59
|
+
const metrics = vitalsRef.current;
|
|
60
|
+
if (Object.keys(metrics).length === 0) return;
|
|
61
|
+
hasSentVitalsRef.current = true;
|
|
62
|
+
const body = JSON.stringify({
|
|
63
|
+
type: "vitals",
|
|
64
|
+
path: window.location.pathname,
|
|
65
|
+
sessionId: sessionIdRef.current,
|
|
66
|
+
meta: { ...metrics }
|
|
67
|
+
});
|
|
68
|
+
if (typeof navigator.sendBeacon === "function") {
|
|
69
|
+
navigator.sendBeacon(
|
|
70
|
+
endpoint,
|
|
71
|
+
new Blob([body], { type: "application/json" })
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
fetch(endpoint, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body,
|
|
78
|
+
keepalive: true
|
|
79
|
+
}).catch(() => {
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function onVisibilityChange() {
|
|
84
|
+
if (document.visibilityState === "hidden") {
|
|
85
|
+
sendVitals();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
89
|
+
window.addEventListener("pagehide", sendVitals);
|
|
90
|
+
return () => {
|
|
91
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
92
|
+
window.removeEventListener("pagehide", sendVitals);
|
|
93
|
+
};
|
|
94
|
+
}, [endpoint, excludePaths]);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
PulseTracker
|
|
99
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { PulseEventPayload } from '@pulsekit/core';
|
|
4
|
+
|
|
5
|
+
interface PulseHandlerConfig {
|
|
6
|
+
supabase: SupabaseClient;
|
|
7
|
+
config?: {
|
|
8
|
+
allowLocalhost?: boolean;
|
|
9
|
+
ignorePaths?: string[];
|
|
10
|
+
siteId?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface PulseRequestBody extends PulseEventPayload {
|
|
14
|
+
siteId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare function createPulseHandler({ supabase, config }: PulseHandlerConfig): (req: NextRequest) => Promise<NextResponse<unknown>>;
|
|
18
|
+
|
|
19
|
+
interface RefreshHandlerConfig {
|
|
20
|
+
supabase: SupabaseClient;
|
|
21
|
+
daysBack?: number;
|
|
22
|
+
}
|
|
23
|
+
declare function createRefreshHandler({ supabase, daysBack, }: RefreshHandlerConfig): () => Promise<NextResponse<{
|
|
24
|
+
error: string;
|
|
25
|
+
}> | NextResponse<{
|
|
26
|
+
ok: boolean;
|
|
27
|
+
}>>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Server-side helper that reads the browser timezone from the cookie
|
|
31
|
+
* set by <PulseTimezone />. Falls back to "UTC" if not set.
|
|
32
|
+
*/
|
|
33
|
+
declare function getPulseTimezone(): Promise<string>;
|
|
34
|
+
|
|
35
|
+
export { type PulseHandlerConfig, type PulseRequestBody, type RefreshHandlerConfig, createPulseHandler, createRefreshHandler, getPulseTimezone };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/createPulseHandler.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var EventSchema = z.object({
|
|
5
|
+
type: z.enum(["pageview", "custom", "vitals"]),
|
|
6
|
+
path: z.string().min(1),
|
|
7
|
+
sessionId: z.string().optional(),
|
|
8
|
+
meta: z.record(z.unknown()).optional(),
|
|
9
|
+
siteId: z.string().optional()
|
|
10
|
+
});
|
|
11
|
+
function createPulseHandler({ supabase, config }) {
|
|
12
|
+
return async function handler(req) {
|
|
13
|
+
if (req.method !== "POST") {
|
|
14
|
+
return new NextResponse("Method Not Allowed", { status: 405 });
|
|
15
|
+
}
|
|
16
|
+
const origin = req.headers.get("origin") ?? "";
|
|
17
|
+
const referer = req.headers.get("referer") ?? "";
|
|
18
|
+
const isLocalhost = origin.includes("localhost") || referer.includes("localhost");
|
|
19
|
+
if (!isLocalhost || config?.allowLocalhost === false) {
|
|
20
|
+
}
|
|
21
|
+
let json;
|
|
22
|
+
try {
|
|
23
|
+
json = await req.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
const parse = EventSchema.safeParse(json);
|
|
28
|
+
if (!parse.success) {
|
|
29
|
+
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
const event = parse.data;
|
|
32
|
+
const siteId = event.siteId ?? config?.siteId ?? "default";
|
|
33
|
+
const country = req.headers.get("x-vercel-ip-country") ?? null;
|
|
34
|
+
const region = req.headers.get("x-vercel-ip-country-region") ?? null;
|
|
35
|
+
const city = req.headers.get("x-vercel-ip-city") ?? null;
|
|
36
|
+
const timezone = req.headers.get("x-vercel-ip-timezone") ?? null;
|
|
37
|
+
const latRaw = req.headers.get("x-vercel-ip-latitude");
|
|
38
|
+
const lngRaw = req.headers.get("x-vercel-ip-longitude");
|
|
39
|
+
const latitude = latRaw ? parseFloat(latRaw) : null;
|
|
40
|
+
const longitude = lngRaw ? parseFloat(lngRaw) : null;
|
|
41
|
+
try {
|
|
42
|
+
const { error } = await supabase.schema("analytics").from("pulse_events").insert({
|
|
43
|
+
site_id: siteId,
|
|
44
|
+
session_id: event.sessionId ?? null,
|
|
45
|
+
path: event.path,
|
|
46
|
+
event_type: event.type,
|
|
47
|
+
meta: event.meta ?? null,
|
|
48
|
+
country,
|
|
49
|
+
region,
|
|
50
|
+
city,
|
|
51
|
+
timezone,
|
|
52
|
+
latitude,
|
|
53
|
+
longitude
|
|
54
|
+
});
|
|
55
|
+
if (error) throw error;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(e);
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: "Failed to log event" },
|
|
60
|
+
{ status: 500 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return NextResponse.json({ ok: true });
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/createRefreshHandler.ts
|
|
68
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
69
|
+
function createRefreshHandler({
|
|
70
|
+
supabase,
|
|
71
|
+
daysBack = 7
|
|
72
|
+
}) {
|
|
73
|
+
return async function handler() {
|
|
74
|
+
const { error } = await supabase.schema("analytics").rpc("pulse_refresh_aggregates", { days_back: daysBack });
|
|
75
|
+
if (error) {
|
|
76
|
+
return NextResponse2.json({ error: error.message }, { status: 500 });
|
|
77
|
+
}
|
|
78
|
+
return NextResponse2.json({ ok: true });
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/getPulseTimezone.ts
|
|
83
|
+
import { cookies } from "next/headers";
|
|
84
|
+
async function getPulseTimezone() {
|
|
85
|
+
const cookieStore = await cookies();
|
|
86
|
+
return cookieStore.get("pulse_tz")?.value ?? "UTC";
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
createPulseHandler,
|
|
90
|
+
createRefreshHandler,
|
|
91
|
+
getPulseTimezone
|
|
92
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulsekit/next",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "./dist/index.mjs",
|
|
5
|
+
"types": "./dist/index.d.mts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.mts"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"import": "./dist/client.mjs",
|
|
16
|
+
"types": "./dist/client.d.mts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"web-vitals": "^4.2.0",
|
|
21
|
+
"zod": "^3.24.0",
|
|
22
|
+
"@pulsekit/core": "0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"next": "^15.0.0",
|
|
27
|
+
"react": "^19.0.0",
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.7.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@supabase/supabase-js": ">=2.0.0",
|
|
33
|
+
"next": ">=14.0.0",
|
|
34
|
+
"react": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"clean": "rm -rf dist"
|
|
40
|
+
}
|
|
41
|
+
}
|