@pulsekit/core 0.0.1 → 0.0.3

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/package.json CHANGED
@@ -1,22 +1,19 @@
1
1
  {
2
2
  "name": "@pulsekit/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
- "files": [
7
- "dist"
8
- ],
6
+ "files": ["dist", "sql"],
9
7
  "exports": {
10
8
  ".": {
11
9
  "import": "./dist/index.mjs",
12
10
  "types": "./dist/index.d.mts"
13
11
  }
14
12
  },
15
- "bin": {
16
- "pulse-init": "./dist/cli.js"
17
- },
18
- "dependencies": {
19
- "postgres": "^3.4.0"
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "clean": "rm -rf dist"
20
17
  },
21
18
  "devDependencies": {
22
19
  "@supabase/supabase-js": "^2.49.0",
@@ -25,10 +22,5 @@
25
22
  },
26
23
  "peerDependencies": {
27
24
  "@supabase/supabase-js": ">=2.0.0"
28
- },
29
- "scripts": {
30
- "build": "tsup",
31
- "dev": "tsup --watch",
32
- "clean": "rm -rf dist"
33
25
  }
34
- }
26
+ }
@@ -0,0 +1,76 @@
1
+ create schema if not exists analytics;
2
+
3
+ -- Add analytics to the schemas exposed by PostgREST
4
+ alter role authenticator set pgrst.db_schemas = 'public, graphql_public, analytics';
5
+
6
+ -- Schema-level access
7
+ grant usage on schema analytics to anon, authenticated, service_role;
8
+ alter default privileges in schema analytics grant all on tables to anon, authenticated, service_role;
9
+
10
+ create table if not exists analytics.pulse_events (
11
+ id bigserial primary key,
12
+ site_id text not null,
13
+ session_id text,
14
+ path text not null,
15
+ event_type text not null,
16
+ meta jsonb,
17
+ created_at timestamptz not null default now()
18
+ );
19
+
20
+ create index if not exists idx_pulse_events_site_created_at
21
+ on analytics.pulse_events (site_id, created_at);
22
+
23
+ create index if not exists idx_pulse_events_site_path_created_at
24
+ on analytics.pulse_events (site_id, path, created_at);
25
+
26
+ alter table analytics.pulse_events enable row level security;
27
+
28
+ -- Allow the anon key (API route) to insert events
29
+ drop policy if exists "Allow anon insert on pulse_events" on analytics.pulse_events;
30
+ create policy "Allow anon insert on pulse_events"
31
+ on analytics.pulse_events
32
+ for insert
33
+ to anon
34
+ with check (true);
35
+
36
+ -- Only authenticated users (dashboard) can read events
37
+ drop policy if exists "Allow authenticated select on pulse_events" on analytics.pulse_events;
38
+ create policy "Allow authenticated select on pulse_events"
39
+ on analytics.pulse_events
40
+ for select
41
+ to authenticated
42
+ using (true);
43
+
44
+ create table if not exists analytics.pulse_aggregates (
45
+ date date not null,
46
+ site_id text not null,
47
+ path text not null,
48
+ total_views integer not null default 0,
49
+ unique_visitors integer not null default 0,
50
+ primary key (date, site_id, path)
51
+ );
52
+
53
+ -- Grant table-level access (must be after table creation)
54
+ grant all on all tables in schema analytics to anon, authenticated, service_role;
55
+ grant all on all sequences in schema analytics to anon, authenticated, service_role;
56
+
57
+ alter table analytics.pulse_aggregates enable row level security;
58
+
59
+ -- Allow reading aggregates (dashboard)
60
+ drop policy if exists "Allow authenticated select on pulse_aggregates" on analytics.pulse_aggregates;
61
+ create policy "Allow authenticated select on pulse_aggregates"
62
+ on analytics.pulse_aggregates
63
+ for select
64
+ to authenticated
65
+ using (true);
66
+
67
+ drop policy if exists "Allow anon select on pulse_aggregates" on analytics.pulse_aggregates;
68
+ create policy "Allow anon select on pulse_aggregates"
69
+ on analytics.pulse_aggregates
70
+ for select
71
+ to anon
72
+ using (true);
73
+
74
+ -- Reload PostgREST config and schema cache (must be last)
75
+ notify pgrst, 'reload config';
76
+ notify pgrst, 'reload schema';
@@ -0,0 +1,25 @@
1
+ -- Aggregation function: rolls up raw events into daily aggregates
2
+ create or replace function analytics.pulse_refresh_aggregates(days_back integer default 7)
3
+ returns void
4
+ language sql
5
+ security definer
6
+ as $$
7
+ insert into analytics.pulse_aggregates (date, site_id, path, total_views, unique_visitors)
8
+ select
9
+ date_trunc('day', created_at)::date as date,
10
+ site_id,
11
+ path,
12
+ count(*) as total_views,
13
+ count(distinct session_id) as unique_visitors
14
+ from analytics.pulse_events
15
+ where created_at >= now() - (days_back || ' days')::interval
16
+ group by 1, 2, 3
17
+ on conflict (date, site_id, path) do update
18
+ set
19
+ total_views = excluded.total_views,
20
+ unique_visitors = excluded.unique_visitors;
21
+ $$;
22
+
23
+ -- Allow all roles to execute the aggregation function
24
+ -- security definer ensures it runs with the owner's privileges regardless of caller
25
+ grant execute on function analytics.pulse_refresh_aggregates(integer) to anon, authenticated, service_role;
@@ -0,0 +1,77 @@
1
+ -- Add geo columns to pulse_events
2
+ alter table analytics.pulse_events
3
+ add column if not exists country text,
4
+ add column if not exists region text,
5
+ add column if not exists city text,
6
+ add column if not exists timezone text,
7
+ add column if not exists latitude double precision,
8
+ add column if not exists longitude double precision;
9
+
10
+ -- Timezone-aware stats: queries raw events with AT TIME ZONE
11
+ -- so the dashboard can display data bucketed by the viewer's local day.
12
+ create or replace function analytics.pulse_stats_by_timezone(
13
+ p_site_id text,
14
+ p_timezone text default 'UTC',
15
+ p_days_back integer default 7
16
+ )
17
+ returns table (
18
+ date date,
19
+ path text,
20
+ total_views bigint,
21
+ unique_visitors bigint
22
+ )
23
+ language sql
24
+ security definer
25
+ stable
26
+ as $$
27
+ select
28
+ date_trunc('day', created_at at time zone p_timezone)::date as date,
29
+ path,
30
+ count(*) as total_views,
31
+ count(distinct session_id) as unique_visitors
32
+ from analytics.pulse_events
33
+ where site_id = p_site_id
34
+ and created_at >= now() - make_interval(days => p_days_back + 1)
35
+ group by 1, 2;
36
+ $$;
37
+
38
+ grant execute on function analytics.pulse_stats_by_timezone(text, text, integer)
39
+ to anon, authenticated, service_role;
40
+
41
+ -- Drop first so return type can change (CREATE OR REPLACE cannot alter return columns)
42
+ drop function if exists analytics.pulse_location_stats(text, integer);
43
+
44
+ -- Location stats: visitor counts grouped by country + city, with averaged coordinates
45
+ create or replace function analytics.pulse_location_stats(
46
+ p_site_id text,
47
+ p_days_back integer default 7
48
+ )
49
+ returns table (
50
+ country text,
51
+ city text,
52
+ latitude double precision,
53
+ longitude double precision,
54
+ total_views bigint,
55
+ unique_visitors bigint
56
+ )
57
+ language sql
58
+ security definer
59
+ stable
60
+ as $$
61
+ select
62
+ country,
63
+ city,
64
+ avg(latitude) as latitude,
65
+ avg(longitude) as longitude,
66
+ count(*) as total_views,
67
+ count(distinct session_id) as unique_visitors
68
+ from analytics.pulse_events
69
+ where site_id = p_site_id
70
+ and created_at >= now() - make_interval(days => p_days_back)
71
+ and country is not null
72
+ group by 1, 2
73
+ order by total_views desc;
74
+ $$;
75
+
76
+ grant execute on function analytics.pulse_location_stats(text, integer)
77
+ to anon, authenticated, service_role;
@@ -0,0 +1,56 @@
1
+ -- 004_web_vitals.sql
2
+ -- Partial index + RPC for Web Vitals p75 aggregation
3
+
4
+ -- Partial index: only covers vitals events, stays small
5
+ CREATE INDEX IF NOT EXISTS idx_pulse_events_vitals
6
+ ON analytics.pulse_events (site_id, created_at)
7
+ WHERE event_type = 'vitals';
8
+
9
+ -- RPC: returns per-metric p75 for each page + site-wide (__overall__)
10
+ CREATE OR REPLACE FUNCTION analytics.pulse_vitals_stats(
11
+ p_site_id TEXT,
12
+ p_days_back INT DEFAULT 7
13
+ )
14
+ RETURNS TABLE (
15
+ path TEXT,
16
+ metric TEXT,
17
+ p75 DOUBLE PRECISION,
18
+ sample_count BIGINT
19
+ )
20
+ LANGUAGE sql SECURITY DEFINER STABLE
21
+ AS $$
22
+ WITH vitals_raw AS (
23
+ SELECT
24
+ e.path,
25
+ kv.key AS metric,
26
+ kv.value::double precision AS val
27
+ FROM analytics.pulse_events e,
28
+ LATERAL jsonb_each_text(e.meta) AS kv(key, value)
29
+ WHERE e.site_id = p_site_id
30
+ AND e.event_type = 'vitals'
31
+ AND e.created_at >= NOW() - (p_days_back || ' days')::interval
32
+ AND kv.key IN ('lcp', 'inp', 'cls', 'fcp', 'ttfb')
33
+ )
34
+ -- Per-page stats
35
+ SELECT
36
+ vr.path,
37
+ vr.metric,
38
+ percentile_cont(0.75) WITHIN GROUP (ORDER BY vr.val) AS p75,
39
+ count(*)::bigint AS sample_count
40
+ FROM vitals_raw vr
41
+ GROUP BY vr.path, vr.metric
42
+
43
+ UNION ALL
44
+
45
+ -- Site-wide stats
46
+ SELECT
47
+ '__overall__'::text AS path,
48
+ vr.metric,
49
+ percentile_cont(0.75) WITHIN GROUP (ORDER BY vr.val) AS p75,
50
+ count(*)::bigint AS sample_count
51
+ FROM vitals_raw vr
52
+ GROUP BY vr.metric;
53
+ $$;
54
+
55
+ GRANT EXECUTE ON FUNCTION analytics.pulse_vitals_stats(TEXT, INT)
56
+ TO anon, authenticated, service_role;
package/dist/cli.js DELETED
@@ -1,47 +0,0 @@
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
- });