@pulsekit/core 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/cli.js +47 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.mjs +139 -0
- package/package.json +34 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_fs = __toESM(require("fs"));
|
|
28
|
+
var import_node_path = __toESM(require("path"));
|
|
29
|
+
var import_postgres = __toESM(require("postgres"));
|
|
30
|
+
async function main() {
|
|
31
|
+
const url = process.env.DATABASE_URL;
|
|
32
|
+
if (!url) throw new Error("DATABASE_URL is required");
|
|
33
|
+
const sql = (0, import_postgres.default)(url, { max: 1 });
|
|
34
|
+
const sqlDir = import_node_path.default.join(__dirname, "../sql");
|
|
35
|
+
const files = import_node_fs.default.readdirSync(sqlDir).filter((f) => f.endsWith(".sql")).sort();
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const text = import_node_fs.default.readFileSync(import_node_path.default.join(sqlDir, file), "utf8");
|
|
38
|
+
await sql.unsafe(text);
|
|
39
|
+
console.log(`Applied: ${file}`);
|
|
40
|
+
}
|
|
41
|
+
await sql.end();
|
|
42
|
+
console.log("Pulse migration complete.");
|
|
43
|
+
}
|
|
44
|
+
main().catch((err) => {
|
|
45
|
+
console.error(err);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
type Timeframe = "7d" | "30d";
|
|
4
|
+
type WebVitalRating = "good" | "needs-improvement" | "poor";
|
|
5
|
+
interface WebVitalStat {
|
|
6
|
+
metric: string;
|
|
7
|
+
p75: number;
|
|
8
|
+
sampleCount: number;
|
|
9
|
+
rating: WebVitalRating;
|
|
10
|
+
}
|
|
11
|
+
interface PageVitalsStat {
|
|
12
|
+
path: string;
|
|
13
|
+
vitals: Record<string, WebVitalStat>;
|
|
14
|
+
}
|
|
15
|
+
interface VitalsOverview {
|
|
16
|
+
overall: WebVitalStat[];
|
|
17
|
+
byPage: PageVitalsStat[];
|
|
18
|
+
}
|
|
19
|
+
interface DailyStat {
|
|
20
|
+
date: string;
|
|
21
|
+
totalViews: number;
|
|
22
|
+
uniqueVisitors: number;
|
|
23
|
+
}
|
|
24
|
+
interface TopPageStat {
|
|
25
|
+
path: string;
|
|
26
|
+
totalViews: number;
|
|
27
|
+
uniqueVisitors: number;
|
|
28
|
+
}
|
|
29
|
+
interface LocationStat {
|
|
30
|
+
country: string;
|
|
31
|
+
city: string | null;
|
|
32
|
+
latitude: number | null;
|
|
33
|
+
longitude: number | null;
|
|
34
|
+
totalViews: number;
|
|
35
|
+
uniqueVisitors: number;
|
|
36
|
+
}
|
|
37
|
+
interface PulseStats {
|
|
38
|
+
daily: DailyStat[];
|
|
39
|
+
topPages: TopPageStat[];
|
|
40
|
+
locations: LocationStat[];
|
|
41
|
+
}
|
|
42
|
+
declare function getPulseStats(opts: {
|
|
43
|
+
supabase: SupabaseClient;
|
|
44
|
+
siteId: string;
|
|
45
|
+
timeframe: Timeframe;
|
|
46
|
+
timezone?: string;
|
|
47
|
+
}): Promise<PulseStats>;
|
|
48
|
+
declare function getPulseVitals(opts: {
|
|
49
|
+
supabase: SupabaseClient;
|
|
50
|
+
siteId: string;
|
|
51
|
+
timeframe: Timeframe;
|
|
52
|
+
}): Promise<VitalsOverview>;
|
|
53
|
+
|
|
54
|
+
type PulseEventType = "pageview" | "custom" | "vitals";
|
|
55
|
+
interface PulseEventPayload {
|
|
56
|
+
type: PulseEventType;
|
|
57
|
+
path: string;
|
|
58
|
+
sessionId?: string;
|
|
59
|
+
meta?: Record<string, unknown>;
|
|
60
|
+
createdAt?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { type DailyStat, type LocationStat, type PageVitalsStat, type PulseEventPayload, type PulseEventType, type PulseStats, type Timeframe, type TopPageStat, type VitalsOverview, type WebVitalRating, type WebVitalStat, getPulseStats, getPulseVitals };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/queries.ts
|
|
2
|
+
var VITAL_THRESHOLDS = {
|
|
3
|
+
lcp: [2500, 4e3],
|
|
4
|
+
inp: [200, 500],
|
|
5
|
+
cls: [0.1, 0.25],
|
|
6
|
+
fcp: [1800, 3e3],
|
|
7
|
+
ttfb: [800, 1800]
|
|
8
|
+
};
|
|
9
|
+
function rateVital(metric, p75) {
|
|
10
|
+
const thresholds = VITAL_THRESHOLDS[metric];
|
|
11
|
+
if (!thresholds) return "good";
|
|
12
|
+
if (p75 <= thresholds[0]) return "good";
|
|
13
|
+
if (p75 <= thresholds[1]) return "needs-improvement";
|
|
14
|
+
return "poor";
|
|
15
|
+
}
|
|
16
|
+
function daysFromTimeframe(timeframe) {
|
|
17
|
+
return timeframe === "7d" ? 7 : 30;
|
|
18
|
+
}
|
|
19
|
+
function cutoffDate(timeframe, timezone) {
|
|
20
|
+
const days = daysFromTimeframe(timeframe);
|
|
21
|
+
const now = /* @__PURE__ */ new Date();
|
|
22
|
+
const localNow = new Date(
|
|
23
|
+
now.toLocaleString("en-US", { timeZone: timezone })
|
|
24
|
+
);
|
|
25
|
+
localNow.setDate(localNow.getDate() - (days - 1));
|
|
26
|
+
return localNow.toISOString().slice(0, 10);
|
|
27
|
+
}
|
|
28
|
+
async function getPulseStats(opts) {
|
|
29
|
+
const { supabase, siteId, timeframe, timezone = "UTC" } = opts;
|
|
30
|
+
const days = daysFromTimeframe(timeframe);
|
|
31
|
+
const since = cutoffDate(timeframe, timezone);
|
|
32
|
+
const { data: rows, error } = await supabase.schema("analytics").rpc("pulse_stats_by_timezone", {
|
|
33
|
+
p_site_id: siteId,
|
|
34
|
+
p_timezone: timezone,
|
|
35
|
+
p_days_back: days
|
|
36
|
+
});
|
|
37
|
+
const { data: locationRows, error: locationError } = await supabase.schema("analytics").rpc("pulse_location_stats", {
|
|
38
|
+
p_site_id: siteId,
|
|
39
|
+
p_days_back: days
|
|
40
|
+
});
|
|
41
|
+
if (error) throw error;
|
|
42
|
+
if (locationError) throw locationError;
|
|
43
|
+
const locations = (locationRows ?? []).map(
|
|
44
|
+
(row) => ({
|
|
45
|
+
country: row.country,
|
|
46
|
+
city: row.city ?? null,
|
|
47
|
+
latitude: row.latitude ?? null,
|
|
48
|
+
longitude: row.longitude ?? null,
|
|
49
|
+
totalViews: Number(row.total_views),
|
|
50
|
+
uniqueVisitors: Number(row.unique_visitors)
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
if (!rows || rows.length === 0) {
|
|
54
|
+
return { daily: [], topPages: [], locations };
|
|
55
|
+
}
|
|
56
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const d = String(row.date);
|
|
59
|
+
if (d < since) continue;
|
|
60
|
+
const existing = byDate.get(d);
|
|
61
|
+
if (existing) {
|
|
62
|
+
existing.views += Number(row.total_views);
|
|
63
|
+
existing.unique += Number(row.unique_visitors);
|
|
64
|
+
} else {
|
|
65
|
+
byDate.set(d, {
|
|
66
|
+
views: Number(row.total_views),
|
|
67
|
+
unique: Number(row.unique_visitors)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const daily = Array.from(byDate.entries()).map(
|
|
72
|
+
([date, { views, unique }]) => ({
|
|
73
|
+
date,
|
|
74
|
+
totalViews: views,
|
|
75
|
+
uniqueVisitors: unique
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const d = String(row.date);
|
|
81
|
+
if (d < since) continue;
|
|
82
|
+
const p = row.path;
|
|
83
|
+
const existing = byPath.get(p);
|
|
84
|
+
if (existing) {
|
|
85
|
+
existing.views += Number(row.total_views);
|
|
86
|
+
existing.unique += Number(row.unique_visitors);
|
|
87
|
+
} else {
|
|
88
|
+
byPath.set(p, {
|
|
89
|
+
views: Number(row.total_views),
|
|
90
|
+
unique: Number(row.unique_visitors)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const topPages = Array.from(byPath.entries()).map(([path, { views, unique }]) => ({
|
|
95
|
+
path,
|
|
96
|
+
totalViews: views,
|
|
97
|
+
uniqueVisitors: unique
|
|
98
|
+
})).sort((a, b) => b.totalViews - a.totalViews).slice(0, 10);
|
|
99
|
+
return { daily, topPages, locations };
|
|
100
|
+
}
|
|
101
|
+
async function getPulseVitals(opts) {
|
|
102
|
+
const { supabase, siteId, timeframe } = opts;
|
|
103
|
+
const days = daysFromTimeframe(timeframe);
|
|
104
|
+
const { data: rows, error } = await supabase.schema("analytics").rpc("pulse_vitals_stats", {
|
|
105
|
+
p_site_id: siteId,
|
|
106
|
+
p_days_back: days
|
|
107
|
+
});
|
|
108
|
+
if (error) throw error;
|
|
109
|
+
const overall = [];
|
|
110
|
+
const pageMap = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const row of rows ?? []) {
|
|
112
|
+
const stat = {
|
|
113
|
+
metric: row.metric,
|
|
114
|
+
p75: Number(row.p75),
|
|
115
|
+
sampleCount: Number(row.sample_count),
|
|
116
|
+
rating: rateVital(row.metric, Number(row.p75))
|
|
117
|
+
};
|
|
118
|
+
if (row.path === "__overall__") {
|
|
119
|
+
overall.push(stat);
|
|
120
|
+
} else {
|
|
121
|
+
let entry = pageMap.get(row.path);
|
|
122
|
+
if (!entry) {
|
|
123
|
+
entry = {};
|
|
124
|
+
pageMap.set(row.path, entry);
|
|
125
|
+
}
|
|
126
|
+
entry[row.metric] = stat;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const byPage = Array.from(pageMap.entries()).map(([path, vitals]) => ({ path, vitals })).sort((a, b) => {
|
|
130
|
+
const aLcp = a.vitals.lcp?.sampleCount ?? 0;
|
|
131
|
+
const bLcp = b.vitals.lcp?.sampleCount ?? 0;
|
|
132
|
+
return bLcp - aLcp;
|
|
133
|
+
}).slice(0, 10);
|
|
134
|
+
return { overall, byPage };
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
getPulseStats,
|
|
138
|
+
getPulseVitals
|
|
139
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulsekit/core",
|
|
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
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"pulse-init": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"postgres": "^3.4.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@supabase/supabase-js": "^2.49.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.7.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@supabase/supabase-js": ">=2.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"clean": "rm -rf dist"
|
|
33
|
+
}
|
|
34
|
+
}
|