@rudderjs/pulse 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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/boost/guidelines.md +33 -0
  4. package/dist/aggregators/cache.d.ts +11 -0
  5. package/dist/aggregators/cache.d.ts.map +1 -0
  6. package/dist/aggregators/cache.js +34 -0
  7. package/dist/aggregators/cache.js.map +1 -0
  8. package/dist/aggregators/exception.d.ts +11 -0
  9. package/dist/aggregators/exception.d.ts.map +1 -0
  10. package/dist/aggregators/exception.js +30 -0
  11. package/dist/aggregators/exception.js.map +1 -0
  12. package/dist/aggregators/query.d.ts +12 -0
  13. package/dist/aggregators/query.d.ts.map +1 -0
  14. package/dist/aggregators/query.js +39 -0
  15. package/dist/aggregators/query.js.map +1 -0
  16. package/dist/aggregators/queue.d.ts +12 -0
  17. package/dist/aggregators/queue.d.ts.map +1 -0
  18. package/dist/aggregators/queue.js +43 -0
  19. package/dist/aggregators/queue.js.map +1 -0
  20. package/dist/aggregators/request.d.ts +15 -0
  21. package/dist/aggregators/request.d.ts.map +1 -0
  22. package/dist/aggregators/request.js +46 -0
  23. package/dist/aggregators/request.js.map +1 -0
  24. package/dist/aggregators/server.d.ts +13 -0
  25. package/dist/aggregators/server.d.ts.map +1 -0
  26. package/dist/aggregators/server.js +35 -0
  27. package/dist/aggregators/server.js.map +1 -0
  28. package/dist/aggregators/user.d.ts +17 -0
  29. package/dist/aggregators/user.d.ts.map +1 -0
  30. package/dist/aggregators/user.js +48 -0
  31. package/dist/aggregators/user.js.map +1 -0
  32. package/dist/api/routes.d.ts +6 -0
  33. package/dist/api/routes.d.ts.map +1 -0
  34. package/dist/api/routes.js +167 -0
  35. package/dist/api/routes.js.map +1 -0
  36. package/dist/index.d.ts +37 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +143 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/storage.d.ts +28 -0
  41. package/dist/storage.d.ts.map +1 -0
  42. package/dist/storage.js +219 -0
  43. package/dist/storage.js.map +1 -0
  44. package/dist/types.d.ts +77 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +19 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/ui/dashboard.d.ts +5 -0
  49. package/dist/ui/dashboard.d.ts.map +1 -0
  50. package/dist/ui/dashboard.js +234 -0
  51. package/dist/ui/dashboard.js.map +1 -0
  52. package/dist/ui/layout.d.ts +6 -0
  53. package/dist/ui/layout.d.ts.map +1 -0
  54. package/dist/ui/layout.js +54 -0
  55. package/dist/ui/layout.js.map +1 -0
  56. package/dist/utils.d.ts +6 -0
  57. package/dist/utils.d.ts.map +1 -0
  58. package/dist/utils.js +19 -0
  59. package/dist/utils.js.map +1 -0
  60. package/package.json +66 -0
@@ -0,0 +1,77 @@
1
+ export type MetricType = 'request_count' | 'request_duration' | 'queue_throughput' | 'queue_wait_time' | 'cache_hits' | 'cache_misses' | 'exceptions' | 'active_users' | 'server_cpu' | 'server_memory';
2
+ export type EntryType = 'slow_request' | 'slow_query' | 'exception' | 'failed_job';
3
+ export interface PulseAggregate {
4
+ id: string;
5
+ bucket: Date;
6
+ type: MetricType;
7
+ key: string | null;
8
+ count: number;
9
+ sum: number;
10
+ min: number | null;
11
+ max: number | null;
12
+ createdAt: Date;
13
+ }
14
+ export interface PulseEntry {
15
+ id: string;
16
+ type: EntryType;
17
+ content: Record<string, unknown>;
18
+ createdAt: Date;
19
+ }
20
+ export interface PulseStorage {
21
+ /** Increment an aggregate bucket. Creates the bucket if it doesn't exist. */
22
+ record(type: MetricType, value: number, key?: string | null): void | Promise<void>;
23
+ /** Store an individual entry (slow request, exception, etc.) */
24
+ storeEntry(type: EntryType, content: Record<string, unknown>): void | Promise<void>;
25
+ /** Get aggregates for a metric type within a time period */
26
+ aggregates(type: MetricType, since: Date, key?: string | null): PulseAggregate[] | Promise<PulseAggregate[]>;
27
+ /** Get individual entries by type */
28
+ entries(type: EntryType, options?: EntryListOptions): PulseEntry[] | Promise<PulseEntry[]>;
29
+ /** Get the overview — latest bucket for each metric type */
30
+ overview(since: Date): PulseAggregate[] | Promise<PulseAggregate[]>;
31
+ /** Delete aggregates and entries older than the given date */
32
+ pruneOlderThan(date: Date): void | Promise<void>;
33
+ }
34
+ export interface EntryListOptions {
35
+ page?: number | undefined;
36
+ perPage?: number | undefined;
37
+ search?: string | undefined;
38
+ }
39
+ export interface Aggregator {
40
+ readonly name: string;
41
+ register(): void | Promise<void>;
42
+ }
43
+ export interface PulseConfig {
44
+ enabled?: boolean | undefined;
45
+ path?: string | undefined;
46
+ storage?: 'memory' | 'sqlite' | undefined;
47
+ sqlitePath?: string | undefined;
48
+ pruneAfterHours?: number | undefined;
49
+ slowRequestThreshold?: number | undefined;
50
+ slowQueryThreshold?: number | undefined;
51
+ recordRequests?: boolean | undefined;
52
+ recordQueues?: boolean | undefined;
53
+ recordCache?: boolean | undefined;
54
+ recordExceptions?: boolean | undefined;
55
+ recordUsers?: boolean | undefined;
56
+ recordServers?: boolean | undefined;
57
+ serverStatsIntervalMs?: number | undefined;
58
+ auth?: null | ((req: unknown) => boolean | Promise<boolean>) | undefined;
59
+ }
60
+ export declare const defaultConfig: {
61
+ enabled: boolean;
62
+ path: string;
63
+ storage: "memory";
64
+ sqlitePath: string;
65
+ pruneAfterHours: number;
66
+ slowRequestThreshold: number;
67
+ slowQueryThreshold: number;
68
+ recordRequests: boolean;
69
+ recordQueues: boolean;
70
+ recordCache: boolean;
71
+ recordExceptions: boolean;
72
+ recordUsers: boolean;
73
+ recordServers: boolean;
74
+ serverStatsIntervalMs: number;
75
+ auth: null | ((req: unknown) => boolean | Promise<boolean>);
76
+ };
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAClB,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,GACjB,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,eAAe,CAAA;AAEnB,MAAM,MAAM,SAAS,GACjB,cAAc,GACd,YAAY,GACZ,WAAW,GACX,YAAY,CAAA;AAIhB,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAS,MAAM,CAAA;IACjB,MAAM,EAAK,IAAI,CAAA;IACf,IAAI,EAAO,UAAU,CAAA;IACrB,GAAG,EAAQ,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAM,MAAM,CAAA;IACjB,GAAG,EAAQ,MAAM,CAAA;IACjB,GAAG,EAAQ,MAAM,GAAG,IAAI,CAAA;IACxB,GAAG,EAAQ,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,IAAI,CAAA;CAChB;AAID,MAAM,WAAW,UAAU;IACzB,EAAE,EAAS,MAAM,CAAA;IACjB,IAAI,EAAO,SAAS,CAAA;IACpB,OAAO,EAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,SAAS,EAAE,IAAI,CAAA;CAChB;AAID,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElF,gEAAgE;IAChE,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnF,4DAA4D;IAC5D,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IAE5G,qCAAqC;IACrC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;IAE1F,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,IAAI,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IAEnE,8DAA8D;IAC9D,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAK,MAAM,GAAG,SAAS,CAAA;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,MAAM,CAAC,EAAG,MAAM,GAAG,SAAS,CAAA;CAC7B;AAID,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC;AAID,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAiB,OAAO,GAAG,SAAS,CAAA;IAC5C,IAAI,CAAC,EAAoB,MAAM,GAAG,SAAS,CAAA;IAC3C,OAAO,CAAC,EAAiB,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;IACxD,UAAU,CAAC,EAAc,MAAM,GAAG,SAAS,CAAA;IAC3C,eAAe,CAAC,EAAS,MAAM,GAAG,SAAS,CAAA;IAC3C,oBAAoB,CAAC,EAAI,MAAM,GAAG,SAAS,CAAA;IAC3C,kBAAkB,CAAC,EAAM,MAAM,GAAG,SAAS,CAAA;IAC3C,cAAc,CAAC,EAAU,OAAO,GAAG,SAAS,CAAA;IAC5C,YAAY,CAAC,EAAY,OAAO,GAAG,SAAS,CAAA;IAC5C,WAAW,CAAC,EAAa,OAAO,GAAG,SAAS,CAAA;IAC5C,gBAAgB,CAAC,EAAQ,OAAO,GAAG,SAAS,CAAA;IAC5C,WAAW,CAAC,EAAa,OAAO,GAAG,SAAS,CAAA;IAC5C,aAAa,CAAC,EAAW,OAAO,GAAG,SAAS,CAAA;IAC5C,qBAAqB,CAAC,EAAG,MAAM,GAAG,SAAS,CAAA;IAC3C,IAAI,CAAC,EAAoB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;CAC3F;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;UAeO,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrF,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,19 @@
1
+ // ─── Metric Types ──────────────────────────────────────────
2
+ export const defaultConfig = {
3
+ enabled: true,
4
+ path: 'pulse',
5
+ storage: 'memory',
6
+ sqlitePath: '.pulse.db',
7
+ pruneAfterHours: 168, // 7 days
8
+ slowRequestThreshold: 1000, // ms
9
+ slowQueryThreshold: 100, // ms
10
+ recordRequests: true,
11
+ recordQueues: true,
12
+ recordCache: true,
13
+ recordExceptions: true,
14
+ recordUsers: true,
15
+ recordServers: true,
16
+ serverStatsIntervalMs: 15_000,
17
+ auth: null,
18
+ };
19
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAkG9D,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,OAAO,EAAgB,IAAI;IAC3B,IAAI,EAAmB,OAAO;IAC9B,OAAO,EAAgB,QAAiB;IACxC,UAAU,EAAa,WAAW;IAClC,eAAe,EAAQ,GAAG,EAAG,SAAS;IACtC,oBAAoB,EAAG,IAAI,EAAE,KAAK;IAClC,kBAAkB,EAAK,GAAG,EAAG,KAAK;IAClC,cAAc,EAAS,IAAI;IAC3B,YAAY,EAAW,IAAI;IAC3B,WAAW,EAAY,IAAI;IAC3B,gBAAgB,EAAO,IAAI;IAC3B,WAAW,EAAY,IAAI;IAC3B,aAAa,EAAU,IAAI;IAC3B,qBAAqB,EAAE,MAAM;IAC7B,IAAI,EAAmB,IAA6D;CACrF,CAAA"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Renders the Pulse dashboard — a single page with metric cards that auto-refresh.
3
+ */
4
+ export declare function dashboardPage(apiPrefix: string): string;
5
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/ui/dashboard.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAoOvD"}
@@ -0,0 +1,234 @@
1
+ import { layout } from './layout.js';
2
+ /**
3
+ * Renders the Pulse dashboard — a single page with metric cards that auto-refresh.
4
+ */
5
+ export function dashboardPage(apiPrefix) {
6
+ return layout('Dashboard', `
7
+ <div x-data="dashboard()" x-init="load()" x-cloak>
8
+ <!-- Metric Cards Grid -->
9
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
10
+ <!-- Request Throughput -->
11
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
12
+ <div class="flex items-center justify-between mb-3">
13
+ <h3 class="text-sm font-medium text-gray-500">Requests</h3>
14
+ <span class="text-xs text-gray-400" x-text="period"></span>
15
+ </div>
16
+ <div class="text-2xl font-bold" x-text="fmt(metrics.request_count?.total || 0)"></div>
17
+ <div class="text-sm text-gray-500 mt-1">
18
+ avg <span x-text="fmt(metrics.request_duration?.avg || 0)"></span>ms
19
+ </div>
20
+ <div class="sparkline mt-3" x-html="sparkline(requestBuckets)"></div>
21
+ </div>
22
+
23
+ <!-- Cache Hit Rate -->
24
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
25
+ <div class="flex items-center justify-between mb-3">
26
+ <h3 class="text-sm font-medium text-gray-500">Cache Hit Rate</h3>
27
+ </div>
28
+ <div class="text-2xl font-bold">
29
+ <span x-text="cacheData.hit_rate || 0"></span>%
30
+ </div>
31
+ <div class="text-sm text-gray-500 mt-1">
32
+ <span x-text="fmt(cacheData.total_hits || 0)"></span> hits /
33
+ <span x-text="fmt(cacheData.total_misses || 0)"></span> misses
34
+ </div>
35
+ <div class="mt-3 h-2 bg-gray-100 rounded-full overflow-hidden">
36
+ <div class="h-full bg-green-500 rounded-full transition-all" :style="'width:' + (cacheData.hit_rate || 0) + '%'"></div>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Queue Throughput -->
41
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
42
+ <div class="flex items-center justify-between mb-3">
43
+ <h3 class="text-sm font-medium text-gray-500">Queue Jobs</h3>
44
+ </div>
45
+ <div class="text-2xl font-bold" x-text="fmt(metrics.queue_throughput?.total || 0)"></div>
46
+ <div class="text-sm text-gray-500 mt-1">
47
+ avg wait <span x-text="fmt(metrics.queue_wait_time?.avg || 0)"></span>ms
48
+ </div>
49
+ <div class="sparkline mt-3" x-html="sparkline(queueBuckets)"></div>
50
+ </div>
51
+
52
+ <!-- Exceptions -->
53
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
54
+ <div class="flex items-center justify-between mb-3">
55
+ <h3 class="text-sm font-medium text-gray-500">Exceptions</h3>
56
+ </div>
57
+ <div class="text-2xl font-bold" :class="(metrics.exceptions?.total || 0) > 0 ? 'text-red-600' : ''"
58
+ x-text="fmt(metrics.exceptions?.total || 0)"></div>
59
+ <div class="text-sm text-gray-500 mt-1">
60
+ min <span x-text="metrics.exceptions?.min ?? 0"></span> /
61
+ max <span x-text="metrics.exceptions?.max ?? 0"></span>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Active Users -->
66
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
67
+ <div class="flex items-center justify-between mb-3">
68
+ <h3 class="text-sm font-medium text-gray-500">Active Users</h3>
69
+ </div>
70
+ <div class="text-2xl font-bold" x-text="fmt(metrics.active_users?.total || 0)"></div>
71
+ <div class="text-sm text-gray-500 mt-1">unique in period</div>
72
+ </div>
73
+
74
+ <!-- Server CPU -->
75
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
76
+ <div class="flex items-center justify-between mb-3">
77
+ <h3 class="text-sm font-medium text-gray-500">CPU Usage</h3>
78
+ </div>
79
+ <div class="text-2xl font-bold">
80
+ <span x-text="(metrics.server_cpu?.avg || 0).toFixed(1)"></span>%
81
+ </div>
82
+ <div class="mt-3 h-2 bg-gray-100 rounded-full overflow-hidden">
83
+ <div class="h-full rounded-full transition-all" :class="(metrics.server_cpu?.avg || 0) > 80 ? 'bg-red-500' : 'bg-blue-500'"
84
+ :style="'width:' + Math.min(metrics.server_cpu?.avg || 0, 100) + '%'"></div>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Server Memory -->
89
+ <div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
90
+ <div class="flex items-center justify-between mb-3">
91
+ <h3 class="text-sm font-medium text-gray-500">Memory Usage</h3>
92
+ </div>
93
+ <div class="text-2xl font-bold">
94
+ <span x-text="(metrics.server_memory?.avg || 0).toFixed(1)"></span>%
95
+ </div>
96
+ <div class="mt-3 h-2 bg-gray-100 rounded-full overflow-hidden">
97
+ <div class="h-full rounded-full transition-all" :class="(metrics.server_memory?.avg || 0) > 80 ? 'bg-red-500' : 'bg-amber-500'"
98
+ :style="'width:' + Math.min(metrics.server_memory?.avg || 0, 100) + '%'"></div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Slow Requests Table -->
104
+ <div class="mt-8 bg-white rounded-xl border border-gray-200 shadow-sm">
105
+ <div class="px-5 py-4 border-b border-gray-100">
106
+ <h3 class="text-sm font-semibold text-gray-700">Slow Requests</h3>
107
+ </div>
108
+ <div class="overflow-x-auto">
109
+ <table class="w-full text-sm">
110
+ <thead class="bg-gray-50 text-gray-500 text-xs uppercase">
111
+ <tr>
112
+ <th class="px-5 py-3 text-left">Method</th>
113
+ <th class="px-5 py-3 text-left">Path</th>
114
+ <th class="px-5 py-3 text-right">Duration</th>
115
+ <th class="px-5 py-3 text-right">Time</th>
116
+ </tr>
117
+ </thead>
118
+ <tbody class="divide-y divide-gray-100">
119
+ <template x-for="r in slowRequests" :key="r.id">
120
+ <tr class="hover:bg-gray-50">
121
+ <td class="px-5 py-3">
122
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
123
+ :class="r.content.method === 'GET' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'"
124
+ x-text="r.content.method"></span>
125
+ </td>
126
+ <td class="px-5 py-3 font-mono text-xs" x-text="r.content.path"></td>
127
+ <td class="px-5 py-3 text-right text-red-600 font-medium" x-text="r.content.duration + 'ms'"></td>
128
+ <td class="px-5 py-3 text-right text-gray-400 text-xs" x-text="ago(r.createdAt)"></td>
129
+ </tr>
130
+ </template>
131
+ <tr x-show="slowRequests.length === 0">
132
+ <td colspan="4" class="px-5 py-8 text-center text-gray-400">No slow requests recorded.</td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Recent Exceptions Table -->
140
+ <div class="mt-6 bg-white rounded-xl border border-gray-200 shadow-sm">
141
+ <div class="px-5 py-4 border-b border-gray-100">
142
+ <h3 class="text-sm font-semibold text-gray-700">Recent Exceptions</h3>
143
+ </div>
144
+ <div class="overflow-x-auto">
145
+ <table class="w-full text-sm">
146
+ <thead class="bg-gray-50 text-gray-500 text-xs uppercase">
147
+ <tr>
148
+ <th class="px-5 py-3 text-left">Class</th>
149
+ <th class="px-5 py-3 text-left">Message</th>
150
+ <th class="px-5 py-3 text-right">Time</th>
151
+ </tr>
152
+ </thead>
153
+ <tbody class="divide-y divide-gray-100">
154
+ <template x-for="e in exceptions" :key="e.id">
155
+ <tr class="hover:bg-gray-50">
156
+ <td class="px-5 py-3 font-mono text-xs text-red-600" x-text="e.content.class"></td>
157
+ <td class="px-5 py-3 text-gray-600 truncate max-w-md" x-text="e.content.message"></td>
158
+ <td class="px-5 py-3 text-right text-gray-400 text-xs" x-text="ago(e.createdAt)"></td>
159
+ </tr>
160
+ </template>
161
+ <tr x-show="exceptions.length === 0">
162
+ <td colspan="3" class="px-5 py-8 text-center text-gray-400">No exceptions recorded.</td>
163
+ </tr>
164
+ </tbody>
165
+ </table>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Auto-refresh indicator -->
170
+ <div class="mt-4 text-center text-xs text-gray-400">
171
+ Auto-refreshes every 10 seconds
172
+ </div>
173
+ </div>
174
+
175
+ <script>
176
+ function dashboard() {
177
+ return {
178
+ period: new URLSearchParams(location.search).get('period') || '1h',
179
+ metrics: {},
180
+ cacheData: {},
181
+ requestBuckets: [],
182
+ queueBuckets: [],
183
+ slowRequests: [],
184
+ exceptions: [],
185
+
186
+ async load() {
187
+ await this.refresh()
188
+ setInterval(() => this.refresh(), 10000)
189
+ },
190
+
191
+ async refresh() {
192
+ const p = '?period=' + this.period
193
+ const [overview, cache, requests, queues, exceptions, slowReqs] = await Promise.all([
194
+ fetch('${apiPrefix}/overview' + p).then(r => r.json()),
195
+ fetch('${apiPrefix}/cache' + p).then(r => r.json()),
196
+ fetch('${apiPrefix}/requests' + p).then(r => r.json()),
197
+ fetch('${apiPrefix}/queues' + p).then(r => r.json()),
198
+ fetch('${apiPrefix}/exceptions' + p).then(r => r.json()),
199
+ fetch('${apiPrefix}/slow-requests?per_page=10').then(r => r.json()),
200
+ ])
201
+ this.metrics = overview.metrics || {}
202
+ this.cacheData = cache
203
+ this.requestBuckets = (requests.throughput || []).map(b => b.count)
204
+ this.queueBuckets = (queues.throughput || []).map(b => b.count)
205
+ this.slowRequests = slowReqs.data || []
206
+ this.exceptions = exceptions.recent || []
207
+ },
208
+
209
+ sparkline(data) {
210
+ if (!data.length) return '<span class="text-xs text-gray-300">No data</span>'
211
+ const max = Math.max(...data, 1)
212
+ return data.slice(-30).map(v =>
213
+ '<div class="bar" style="height:' + Math.max((v / max) * 100, 2) + '%"></div>'
214
+ ).join('')
215
+ },
216
+
217
+ fmt(n) {
218
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
219
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
220
+ return Math.round(n)
221
+ },
222
+
223
+ ago(dateStr) {
224
+ const s = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000)
225
+ if (s < 60) return s + 's ago'
226
+ if (s < 3600) return Math.floor(s / 60) + 'm ago'
227
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago'
228
+ return Math.floor(s / 86400) + 'd ago'
229
+ }
230
+ }
231
+ }
232
+ </script>`, 'pulse');
233
+ }
234
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../src/ui/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,OAAO,MAAM,CAAC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA4LN,SAAS;uBACT,SAAS;uBACT,SAAS;uBACT,SAAS;uBACT,SAAS;uBACT,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiClB,EAAE,OAAO,CAAC,CAAA;AACxB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared HTML layout for Pulse UI pages.
3
+ * Uses Tailwind CSS (CDN) + Alpine.js (CDN) for a zero-build UI.
4
+ */
5
+ export declare function layout(title: string, body: string, path: string): string;
6
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/ui/layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgDxE"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared HTML layout for Pulse UI pages.
3
+ * Uses Tailwind CSS (CDN) + Alpine.js (CDN) for a zero-build UI.
4
+ */
5
+ export function layout(title, body, path) {
6
+ return `<!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>${title} — Pulse</title>
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
14
+ <style>
15
+ [x-cloak] { display: none !important; }
16
+ .sparkline { display: flex; align-items: end; gap: 1px; height: 40px; }
17
+ .sparkline .bar { flex: 1; background: #6366f1; border-radius: 1px; min-width: 2px; transition: height 0.3s; }
18
+ .sparkline .bar:hover { background: #818cf8; }
19
+ </style>
20
+ </head>
21
+ <body class="bg-gray-50 text-gray-900 font-sans antialiased">
22
+ <div class="min-h-screen">
23
+ <!-- Header -->
24
+ <header class="bg-white border-b border-gray-200 px-6 py-4">
25
+ <div class="max-w-7xl mx-auto flex items-center justify-between">
26
+ <div class="flex items-center gap-3">
27
+ <div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
28
+ <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
29
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
30
+ </svg>
31
+ </div>
32
+ <h1 class="text-lg font-semibold">Pulse</h1>
33
+ </div>
34
+ <div x-data="{ period: new URLSearchParams(location.search).get('period') || '1h' }">
35
+ <select x-model="period" @change="location.search = '?period=' + period"
36
+ class="text-sm border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-1.5 border">
37
+ <option value="1h">Last hour</option>
38
+ <option value="6h">Last 6 hours</option>
39
+ <option value="24h">Last 24 hours</option>
40
+ <option value="7d">Last 7 days</option>
41
+ </select>
42
+ </div>
43
+ </div>
44
+ </header>
45
+
46
+ <!-- Content -->
47
+ <main class="max-w-7xl mx-auto px-6 py-8">
48
+ ${body}
49
+ </main>
50
+ </div>
51
+ </body>
52
+ </html>`;
53
+ }
54
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/ui/layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,IAAY,EAAE,IAAY;IAC9D,OAAO;;;;;WAKE,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqCR,IAAI;;;;QAIJ,CAAA;AACR,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * True for Vite internals, source files, and static assets that should be excluded
3
+ * from request metrics. Used by request and user aggregators.
4
+ */
5
+ export declare function isAsset(path: string): boolean;
6
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ7C"}
package/dist/utils.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * True for Vite internals, source files, and static assets that should be excluded
3
+ * from request metrics. Used by request and user aggregators.
4
+ */
5
+ export function isAsset(path) {
6
+ if (path.startsWith('/@'))
7
+ return true; // Vite internals: /@vite, /@react-refresh, /@id, /@fs
8
+ if (path.startsWith('/node_modules'))
9
+ return true;
10
+ if (path.startsWith('/src/'))
11
+ return true; // Vite source modules during dev
12
+ if (path.startsWith('/pages/'))
13
+ return true; // Vike page modules during dev
14
+ if (path.startsWith('/.vite/'))
15
+ return true; // Vite cache
16
+ const segment = path.split('/').pop() ?? '';
17
+ return segment.includes('.'); // any file extension → static asset
18
+ }
19
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAa,OAAO,IAAI,CAAA,CAAE,sDAAsD;IACzG,IAAI,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAA;IACjD,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAU,OAAO,IAAI,CAAA,CAAE,iCAAiC;IACpF,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAQ,OAAO,IAAI,CAAA,CAAE,+BAA+B;IAClF,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAQ,OAAO,IAAI,CAAA,CAAE,aAAa;IAChE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;IAC3C,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA,CAAuB,oCAAoC;AACzF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@rudderjs/pulse",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/rudderjs/rudder",
8
+ "directory": "packages/pulse"
9
+ },
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "boost"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "@rudderjs/contracts": "0.0.3",
25
+ "@rudderjs/core": "0.0.8"
26
+ },
27
+ "peerDependencies": {
28
+ "@rudderjs/router": "0.0.3",
29
+ "@rudderjs/middleware": "0.0.7",
30
+ "@rudderjs/log": "0.0.1",
31
+ "@rudderjs/orm": "0.0.6",
32
+ "@rudderjs/cache": "0.0.6",
33
+ "@rudderjs/queue": "0.0.5"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@rudderjs/log": {
37
+ "optional": true
38
+ },
39
+ "@rudderjs/orm": {
40
+ "optional": true
41
+ },
42
+ "@rudderjs/cache": {
43
+ "optional": true
44
+ },
45
+ "@rudderjs/queue": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "optionalDependencies": {
50
+ "better-sqlite3": "^11.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.0.0",
54
+ "@types/better-sqlite3": "^7.6.0",
55
+ "typescript": "^5.4.0"
56
+ },
57
+ "author": "Suleiman Shahbari",
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json",
60
+ "dev": "tsc -p tsconfig.build.json --watch",
61
+ "typecheck": "tsc --noEmit",
62
+ "lint": "eslint src",
63
+ "clean": "rm -rf dist",
64
+ "test": "tsc -p tsconfig.test.json && node --test dist-test/index.test.js; EXIT=$?; rm -rf dist-test; exit $EXIT"
65
+ }
66
+ }