tersejson 0.1.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/LICENSE +21 -0
- package/README.md +476 -0
- package/dist/analytics.d.mts +186 -0
- package/dist/analytics.d.ts +186 -0
- package/dist/analytics.js +226 -0
- package/dist/analytics.js.map +1 -0
- package/dist/analytics.mjs +217 -0
- package/dist/analytics.mjs.map +1 -0
- package/dist/client-BQAZg7I8.d.mts +138 -0
- package/dist/client-DOOGwp_p.d.ts +138 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +200 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +188 -0
- package/dist/client.mjs.map +1 -0
- package/dist/express-BoL__Ao6.d.mts +67 -0
- package/dist/express-LSVylWpN.d.ts +67 -0
- package/dist/express.d.mts +4 -0
- package/dist/express.d.ts +4 -0
- package/dist/express.js +522 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +512 -0
- package/dist/express.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1001 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +985 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations-7WeFO1Lk.d.mts +264 -0
- package/dist/integrations-7WeFO1Lk.d.ts +264 -0
- package/dist/integrations.d.mts +1 -0
- package/dist/integrations.d.ts +1 -0
- package/dist/integrations.js +381 -0
- package/dist/integrations.js.map +1 -0
- package/dist/integrations.mjs +367 -0
- package/dist/integrations.mjs.map +1 -0
- package/dist/types-CzaGQaV7.d.mts +134 -0
- package/dist/types-CzaGQaV7.d.ts +134 -0
- package/package.json +87 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerseJSON Analytics
|
|
3
|
+
*
|
|
4
|
+
* Opt-in analytics to track compression savings.
|
|
5
|
+
* Data is anonymous and helps improve the library.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Compression event data
|
|
9
|
+
*/
|
|
10
|
+
interface CompressionEvent {
|
|
11
|
+
/** Timestamp of the compression */
|
|
12
|
+
timestamp: number;
|
|
13
|
+
/** Original payload size in bytes */
|
|
14
|
+
originalSize: number;
|
|
15
|
+
/** Compressed payload size in bytes */
|
|
16
|
+
compressedSize: number;
|
|
17
|
+
/** Number of objects in the array */
|
|
18
|
+
objectCount: number;
|
|
19
|
+
/** Number of keys compressed */
|
|
20
|
+
keysCompressed: number;
|
|
21
|
+
/** Route/endpoint (optional, anonymized) */
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
/** Key pattern used */
|
|
24
|
+
keyPattern: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Aggregated stats for reporting
|
|
28
|
+
*/
|
|
29
|
+
interface AnalyticsStats {
|
|
30
|
+
/** Total compression events */
|
|
31
|
+
totalEvents: number;
|
|
32
|
+
/** Total bytes before compression */
|
|
33
|
+
totalOriginalBytes: number;
|
|
34
|
+
/** Total bytes after compression */
|
|
35
|
+
totalCompressedBytes: number;
|
|
36
|
+
/** Total bytes saved */
|
|
37
|
+
totalBytesSaved: number;
|
|
38
|
+
/** Average compression ratio (0-1) */
|
|
39
|
+
averageRatio: number;
|
|
40
|
+
/** Total objects processed */
|
|
41
|
+
totalObjects: number;
|
|
42
|
+
/** Session start time */
|
|
43
|
+
sessionStart: number;
|
|
44
|
+
/** Last event time */
|
|
45
|
+
lastEvent: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Analytics configuration
|
|
49
|
+
*/
|
|
50
|
+
interface AnalyticsConfig {
|
|
51
|
+
/**
|
|
52
|
+
* Enable analytics collection
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Send anonymous stats to tersejson.com
|
|
58
|
+
* Helps improve the library
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
reportToCloud: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* API key for tersejson.com (optional)
|
|
64
|
+
* Get one at tersejson.com/dashboard
|
|
65
|
+
*/
|
|
66
|
+
apiKey?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Project/site identifier (optional)
|
|
69
|
+
*/
|
|
70
|
+
projectId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Callback for each compression event
|
|
73
|
+
* Use for custom logging/monitoring
|
|
74
|
+
*/
|
|
75
|
+
onEvent?: (event: CompressionEvent) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Callback for periodic stats summary
|
|
78
|
+
*/
|
|
79
|
+
onStats?: (stats: AnalyticsStats) => void;
|
|
80
|
+
/**
|
|
81
|
+
* How often to report stats (ms)
|
|
82
|
+
* @default 60000 (1 minute)
|
|
83
|
+
*/
|
|
84
|
+
reportInterval?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Include endpoint paths in analytics
|
|
87
|
+
* Paths are hashed for privacy
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
trackEndpoints?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Cloud reporting endpoint
|
|
93
|
+
* @default 'https://api.tersejson.com/v1/analytics'
|
|
94
|
+
*/
|
|
95
|
+
endpoint?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Enable debug logging
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
debug?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Analytics collector class
|
|
104
|
+
*/
|
|
105
|
+
declare class TerseAnalytics {
|
|
106
|
+
private config;
|
|
107
|
+
private events;
|
|
108
|
+
private stats;
|
|
109
|
+
private reportTimer?;
|
|
110
|
+
private isNode;
|
|
111
|
+
constructor(config?: Partial<AnalyticsConfig>);
|
|
112
|
+
/**
|
|
113
|
+
* Create empty stats object
|
|
114
|
+
*/
|
|
115
|
+
private createEmptyStats;
|
|
116
|
+
/**
|
|
117
|
+
* Record a compression event
|
|
118
|
+
*/
|
|
119
|
+
record(event: Omit<CompressionEvent, 'timestamp'>): void;
|
|
120
|
+
/**
|
|
121
|
+
* Get current stats
|
|
122
|
+
*/
|
|
123
|
+
getStats(): AnalyticsStats;
|
|
124
|
+
/**
|
|
125
|
+
* Get formatted stats summary
|
|
126
|
+
*/
|
|
127
|
+
getSummary(): string;
|
|
128
|
+
/**
|
|
129
|
+
* Reset stats
|
|
130
|
+
*/
|
|
131
|
+
reset(): void;
|
|
132
|
+
/**
|
|
133
|
+
* Start periodic reporting to cloud
|
|
134
|
+
*/
|
|
135
|
+
private startReporting;
|
|
136
|
+
/**
|
|
137
|
+
* Stop reporting
|
|
138
|
+
*/
|
|
139
|
+
stop(): void;
|
|
140
|
+
/**
|
|
141
|
+
* Report stats to tersejson.com
|
|
142
|
+
*/
|
|
143
|
+
private reportToCloud;
|
|
144
|
+
/**
|
|
145
|
+
* Hash endpoint for privacy
|
|
146
|
+
*/
|
|
147
|
+
private hashEndpoint;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Initialize global analytics
|
|
151
|
+
*/
|
|
152
|
+
declare function initAnalytics(config: Partial<AnalyticsConfig>): TerseAnalytics;
|
|
153
|
+
/**
|
|
154
|
+
* Get global analytics instance
|
|
155
|
+
*/
|
|
156
|
+
declare function getAnalytics(): TerseAnalytics | null;
|
|
157
|
+
/**
|
|
158
|
+
* Record an event to global analytics (if initialized)
|
|
159
|
+
*/
|
|
160
|
+
declare function recordEvent(event: Omit<CompressionEvent, 'timestamp'>): void;
|
|
161
|
+
/**
|
|
162
|
+
* Quick setup for common use cases
|
|
163
|
+
*/
|
|
164
|
+
declare const analytics: {
|
|
165
|
+
/**
|
|
166
|
+
* Enable local-only analytics (no cloud reporting)
|
|
167
|
+
*/
|
|
168
|
+
local(options?: {
|
|
169
|
+
debug?: boolean;
|
|
170
|
+
onEvent?: AnalyticsConfig["onEvent"];
|
|
171
|
+
}): TerseAnalytics;
|
|
172
|
+
/**
|
|
173
|
+
* Enable cloud analytics with API key
|
|
174
|
+
*/
|
|
175
|
+
cloud(apiKey: string, options?: Partial<AnalyticsConfig>): TerseAnalytics;
|
|
176
|
+
/**
|
|
177
|
+
* Get current stats
|
|
178
|
+
*/
|
|
179
|
+
getStats(): AnalyticsStats | null;
|
|
180
|
+
/**
|
|
181
|
+
* Get formatted summary
|
|
182
|
+
*/
|
|
183
|
+
getSummary(): string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export { type AnalyticsConfig, type AnalyticsStats, type CompressionEvent, TerseAnalytics, analytics, analytics as default, getAnalytics, initAnalytics, recordEvent };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
// src/analytics.ts
|
|
6
|
+
var DEFAULT_CONFIG = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
reportToCloud: false,
|
|
9
|
+
reportInterval: 6e4,
|
|
10
|
+
trackEndpoints: false,
|
|
11
|
+
endpoint: "https://api.tersejson.com/v1/analytics",
|
|
12
|
+
debug: false
|
|
13
|
+
};
|
|
14
|
+
var TerseAnalytics = class {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
this.events = [];
|
|
17
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
18
|
+
this.isNode = typeof window === "undefined";
|
|
19
|
+
this.stats = this.createEmptyStats();
|
|
20
|
+
if (this.config.enabled && this.config.reportToCloud) {
|
|
21
|
+
this.startReporting();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create empty stats object
|
|
26
|
+
*/
|
|
27
|
+
createEmptyStats() {
|
|
28
|
+
return {
|
|
29
|
+
totalEvents: 0,
|
|
30
|
+
totalOriginalBytes: 0,
|
|
31
|
+
totalCompressedBytes: 0,
|
|
32
|
+
totalBytesSaved: 0,
|
|
33
|
+
averageRatio: 0,
|
|
34
|
+
totalObjects: 0,
|
|
35
|
+
sessionStart: Date.now(),
|
|
36
|
+
lastEvent: Date.now()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Record a compression event
|
|
41
|
+
*/
|
|
42
|
+
record(event) {
|
|
43
|
+
if (!this.config.enabled) return;
|
|
44
|
+
const fullEvent = {
|
|
45
|
+
...event,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
// Hash endpoint for privacy if tracking is enabled
|
|
48
|
+
endpoint: this.config.trackEndpoints && event.endpoint ? this.hashEndpoint(event.endpoint) : void 0
|
|
49
|
+
};
|
|
50
|
+
this.stats.totalEvents++;
|
|
51
|
+
this.stats.totalOriginalBytes += event.originalSize;
|
|
52
|
+
this.stats.totalCompressedBytes += event.compressedSize;
|
|
53
|
+
this.stats.totalBytesSaved += event.originalSize - event.compressedSize;
|
|
54
|
+
this.stats.totalObjects += event.objectCount;
|
|
55
|
+
this.stats.lastEvent = fullEvent.timestamp;
|
|
56
|
+
this.stats.averageRatio = this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;
|
|
57
|
+
this.events.push(fullEvent);
|
|
58
|
+
if (this.config.onEvent) {
|
|
59
|
+
this.config.onEvent(fullEvent);
|
|
60
|
+
}
|
|
61
|
+
if (this.config.debug) {
|
|
62
|
+
const savings = ((1 - event.compressedSize / event.originalSize) * 100).toFixed(1);
|
|
63
|
+
console.log(`[tersejson:analytics] ${event.originalSize} \u2192 ${event.compressedSize} bytes (${savings}% saved)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get current stats
|
|
68
|
+
*/
|
|
69
|
+
getStats() {
|
|
70
|
+
return { ...this.stats };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get formatted stats summary
|
|
74
|
+
*/
|
|
75
|
+
getSummary() {
|
|
76
|
+
const stats = this.stats;
|
|
77
|
+
const savedKB = (stats.totalBytesSaved / 1024).toFixed(2);
|
|
78
|
+
const savedPercent = stats.totalOriginalBytes > 0 ? ((1 - stats.averageRatio) * 100).toFixed(1) : "0";
|
|
79
|
+
return `TerseJSON Stats: ${stats.totalEvents} compressions, ${savedKB}KB saved (${savedPercent}% avg)`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reset stats
|
|
83
|
+
*/
|
|
84
|
+
reset() {
|
|
85
|
+
this.events = [];
|
|
86
|
+
this.stats = this.createEmptyStats();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Start periodic reporting to cloud
|
|
90
|
+
*/
|
|
91
|
+
startReporting() {
|
|
92
|
+
if (this.reportTimer) return;
|
|
93
|
+
this.reportTimer = setInterval(() => {
|
|
94
|
+
this.reportToCloud();
|
|
95
|
+
}, this.config.reportInterval);
|
|
96
|
+
if (this.isNode && typeof process !== "undefined") {
|
|
97
|
+
process.on("beforeExit", () => this.reportToCloud());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Stop reporting
|
|
102
|
+
*/
|
|
103
|
+
stop() {
|
|
104
|
+
if (this.reportTimer) {
|
|
105
|
+
clearInterval(this.reportTimer);
|
|
106
|
+
this.reportTimer = void 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Report stats to tersejson.com
|
|
111
|
+
*/
|
|
112
|
+
async reportToCloud() {
|
|
113
|
+
if (!this.config.reportToCloud || this.events.length === 0) return;
|
|
114
|
+
const payload = {
|
|
115
|
+
apiKey: this.config.apiKey,
|
|
116
|
+
projectId: this.config.projectId,
|
|
117
|
+
stats: this.stats,
|
|
118
|
+
events: this.events.slice(-100),
|
|
119
|
+
// Last 100 events only
|
|
120
|
+
meta: {
|
|
121
|
+
version: "0.1.0",
|
|
122
|
+
runtime: this.isNode ? "node" : "browser"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
const fetchFn = this.isNode ? (await import('https')).request : globalThis.fetch;
|
|
127
|
+
if (this.isNode) {
|
|
128
|
+
const url = new URL(this.config.endpoint);
|
|
129
|
+
const req = fetchFn({
|
|
130
|
+
hostname: url.hostname,
|
|
131
|
+
port: url.port || 443,
|
|
132
|
+
path: url.pathname,
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
req.write(JSON.stringify(payload));
|
|
139
|
+
req.end();
|
|
140
|
+
} else {
|
|
141
|
+
fetchFn(this.config.endpoint, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify(payload),
|
|
145
|
+
keepalive: true
|
|
146
|
+
// Allow sending on page unload
|
|
147
|
+
}).catch(() => {
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
this.events = [];
|
|
151
|
+
if (this.config.onStats) {
|
|
152
|
+
this.config.onStats(this.stats);
|
|
153
|
+
}
|
|
154
|
+
if (this.config.debug) {
|
|
155
|
+
console.log("[tersejson:analytics] Reported stats to cloud");
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
if (this.config.debug) {
|
|
159
|
+
console.log("[tersejson:analytics] Failed to report stats");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Hash endpoint for privacy
|
|
165
|
+
*/
|
|
166
|
+
hashEndpoint(endpoint) {
|
|
167
|
+
return endpoint.replace(/\/\d+/g, "/:id").replace(/\/[a-f0-9-]{36}/gi, "/:uuid").replace(/\?.*$/, "");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var globalAnalytics = null;
|
|
171
|
+
function initAnalytics(config) {
|
|
172
|
+
globalAnalytics = new TerseAnalytics(config);
|
|
173
|
+
return globalAnalytics;
|
|
174
|
+
}
|
|
175
|
+
function getAnalytics() {
|
|
176
|
+
return globalAnalytics;
|
|
177
|
+
}
|
|
178
|
+
function recordEvent(event) {
|
|
179
|
+
globalAnalytics?.record(event);
|
|
180
|
+
}
|
|
181
|
+
var analytics = {
|
|
182
|
+
/**
|
|
183
|
+
* Enable local-only analytics (no cloud reporting)
|
|
184
|
+
*/
|
|
185
|
+
local(options = {}) {
|
|
186
|
+
return initAnalytics({
|
|
187
|
+
enabled: true,
|
|
188
|
+
reportToCloud: false,
|
|
189
|
+
debug: options.debug,
|
|
190
|
+
onEvent: options.onEvent
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* Enable cloud analytics with API key
|
|
195
|
+
*/
|
|
196
|
+
cloud(apiKey, options = {}) {
|
|
197
|
+
return initAnalytics({
|
|
198
|
+
...options,
|
|
199
|
+
enabled: true,
|
|
200
|
+
reportToCloud: true,
|
|
201
|
+
apiKey
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* Get current stats
|
|
206
|
+
*/
|
|
207
|
+
getStats() {
|
|
208
|
+
return globalAnalytics?.getStats() ?? null;
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* Get formatted summary
|
|
212
|
+
*/
|
|
213
|
+
getSummary() {
|
|
214
|
+
return globalAnalytics?.getSummary() ?? "Analytics not initialized";
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var analytics_default = analytics;
|
|
218
|
+
|
|
219
|
+
exports.TerseAnalytics = TerseAnalytics;
|
|
220
|
+
exports.analytics = analytics;
|
|
221
|
+
exports.default = analytics_default;
|
|
222
|
+
exports.getAnalytics = getAnalytics;
|
|
223
|
+
exports.initAnalytics = initAnalytics;
|
|
224
|
+
exports.recordEvent = recordEvent;
|
|
225
|
+
//# sourceMappingURL=analytics.js.map
|
|
226
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analytics.ts"],"names":[],"mappings":"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","file":"analytics.js","sourcesContent":["/**\n * TerseJSON Analytics\n *\n * Opt-in analytics to track compression savings.\n * Data is anonymous and helps improve the library.\n */\n\n/**\n * Compression event data\n */\nexport interface CompressionEvent {\n /** Timestamp of the compression */\n timestamp: number;\n /** Original payload size in bytes */\n originalSize: number;\n /** Compressed payload size in bytes */\n compressedSize: number;\n /** Number of objects in the array */\n objectCount: number;\n /** Number of keys compressed */\n keysCompressed: number;\n /** Route/endpoint (optional, anonymized) */\n endpoint?: string;\n /** Key pattern used */\n keyPattern: string;\n}\n\n/**\n * Aggregated stats for reporting\n */\nexport interface AnalyticsStats {\n /** Total compression events */\n totalEvents: number;\n /** Total bytes before compression */\n totalOriginalBytes: number;\n /** Total bytes after compression */\n totalCompressedBytes: number;\n /** Total bytes saved */\n totalBytesSaved: number;\n /** Average compression ratio (0-1) */\n averageRatio: number;\n /** Total objects processed */\n totalObjects: number;\n /** Session start time */\n sessionStart: number;\n /** Last event time */\n lastEvent: number;\n}\n\n/**\n * Analytics configuration\n */\nexport interface AnalyticsConfig {\n /**\n * Enable analytics collection\n * @default false\n */\n enabled: boolean;\n\n /**\n * Send anonymous stats to tersejson.com\n * Helps improve the library\n * @default false\n */\n reportToCloud: boolean;\n\n /**\n * API key for tersejson.com (optional)\n * Get one at tersejson.com/dashboard\n */\n apiKey?: string;\n\n /**\n * Project/site identifier (optional)\n */\n projectId?: string;\n\n /**\n * Callback for each compression event\n * Use for custom logging/monitoring\n */\n onEvent?: (event: CompressionEvent) => void;\n\n /**\n * Callback for periodic stats summary\n */\n onStats?: (stats: AnalyticsStats) => void;\n\n /**\n * How often to report stats (ms)\n * @default 60000 (1 minute)\n */\n reportInterval?: number;\n\n /**\n * Include endpoint paths in analytics\n * Paths are hashed for privacy\n * @default false\n */\n trackEndpoints?: boolean;\n\n /**\n * Cloud reporting endpoint\n * @default 'https://api.tersejson.com/v1/analytics'\n */\n endpoint?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: Required<Omit<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>> = {\n enabled: false,\n reportToCloud: false,\n reportInterval: 60000,\n trackEndpoints: false,\n endpoint: 'https://api.tersejson.com/v1/analytics',\n debug: false,\n};\n\n/**\n * Analytics collector class\n */\nexport class TerseAnalytics {\n private config: Required<Omit<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>> &\n Pick<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>;\n private events: CompressionEvent[] = [];\n private stats: AnalyticsStats;\n private reportTimer?: ReturnType<typeof setInterval>;\n private isNode: boolean;\n\n constructor(config: Partial<AnalyticsConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.isNode = typeof window === 'undefined';\n this.stats = this.createEmptyStats();\n\n if (this.config.enabled && this.config.reportToCloud) {\n this.startReporting();\n }\n }\n\n /**\n * Create empty stats object\n */\n private createEmptyStats(): AnalyticsStats {\n return {\n totalEvents: 0,\n totalOriginalBytes: 0,\n totalCompressedBytes: 0,\n totalBytesSaved: 0,\n averageRatio: 0,\n totalObjects: 0,\n sessionStart: Date.now(),\n lastEvent: Date.now(),\n };\n }\n\n /**\n * Record a compression event\n */\n record(event: Omit<CompressionEvent, 'timestamp'>): void {\n if (!this.config.enabled) return;\n\n const fullEvent: CompressionEvent = {\n ...event,\n timestamp: Date.now(),\n // Hash endpoint for privacy if tracking is enabled\n endpoint: this.config.trackEndpoints && event.endpoint\n ? this.hashEndpoint(event.endpoint)\n : undefined,\n };\n\n // Update stats\n this.stats.totalEvents++;\n this.stats.totalOriginalBytes += event.originalSize;\n this.stats.totalCompressedBytes += event.compressedSize;\n this.stats.totalBytesSaved += (event.originalSize - event.compressedSize);\n this.stats.totalObjects += event.objectCount;\n this.stats.lastEvent = fullEvent.timestamp;\n this.stats.averageRatio = this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;\n\n // Store event for batch reporting\n this.events.push(fullEvent);\n\n // Call event callback\n if (this.config.onEvent) {\n this.config.onEvent(fullEvent);\n }\n\n // Debug logging\n if (this.config.debug) {\n const savings = ((1 - event.compressedSize / event.originalSize) * 100).toFixed(1);\n console.log(`[tersejson:analytics] ${event.originalSize} → ${event.compressedSize} bytes (${savings}% saved)`);\n }\n }\n\n /**\n * Get current stats\n */\n getStats(): AnalyticsStats {\n return { ...this.stats };\n }\n\n /**\n * Get formatted stats summary\n */\n getSummary(): string {\n const stats = this.stats;\n const savedKB = (stats.totalBytesSaved / 1024).toFixed(2);\n const savedPercent = stats.totalOriginalBytes > 0\n ? ((1 - stats.averageRatio) * 100).toFixed(1)\n : '0';\n\n return `TerseJSON Stats: ${stats.totalEvents} compressions, ${savedKB}KB saved (${savedPercent}% avg)`;\n }\n\n /**\n * Reset stats\n */\n reset(): void {\n this.events = [];\n this.stats = this.createEmptyStats();\n }\n\n /**\n * Start periodic reporting to cloud\n */\n private startReporting(): void {\n if (this.reportTimer) return;\n\n this.reportTimer = setInterval(() => {\n this.reportToCloud();\n }, this.config.reportInterval);\n\n // Report on exit (Node.js only)\n if (this.isNode && typeof process !== 'undefined') {\n process.on('beforeExit', () => this.reportToCloud());\n }\n }\n\n /**\n * Stop reporting\n */\n stop(): void {\n if (this.reportTimer) {\n clearInterval(this.reportTimer);\n this.reportTimer = undefined;\n }\n }\n\n /**\n * Report stats to tersejson.com\n */\n private async reportToCloud(): Promise<void> {\n if (!this.config.reportToCloud || this.events.length === 0) return;\n\n const payload = {\n apiKey: this.config.apiKey,\n projectId: this.config.projectId,\n stats: this.stats,\n events: this.events.slice(-100), // Last 100 events only\n meta: {\n version: '0.1.0',\n runtime: this.isNode ? 'node' : 'browser',\n },\n };\n\n try {\n // Use appropriate fetch based on environment\n const fetchFn = this.isNode\n ? (await import('node:https')).request\n : globalThis.fetch;\n\n if (this.isNode) {\n // Node.js - fire and forget\n const url = new URL(this.config.endpoint);\n const req = (fetchFn as typeof import('node:https').request)({\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n req.write(JSON.stringify(payload));\n req.end();\n } else {\n // Browser\n (fetchFn as typeof fetch)(this.config.endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true, // Allow sending on page unload\n }).catch(() => {}); // Ignore errors\n }\n\n // Clear reported events\n this.events = [];\n\n // Call stats callback\n if (this.config.onStats) {\n this.config.onStats(this.stats);\n }\n\n if (this.config.debug) {\n console.log('[tersejson:analytics] Reported stats to cloud');\n }\n } catch {\n // Silently fail - analytics should never break the app\n if (this.config.debug) {\n console.log('[tersejson:analytics] Failed to report stats');\n }\n }\n }\n\n /**\n * Hash endpoint for privacy\n */\n private hashEndpoint(endpoint: string): string {\n // Simple hash - just keeps route structure without specifics\n return endpoint\n .replace(/\\/\\d+/g, '/:id')\n .replace(/\\/[a-f0-9-]{36}/gi, '/:uuid')\n .replace(/\\?.*$/, '');\n }\n}\n\n/**\n * Global analytics instance (singleton)\n */\nlet globalAnalytics: TerseAnalytics | null = null;\n\n/**\n * Initialize global analytics\n */\nexport function initAnalytics(config: Partial<AnalyticsConfig>): TerseAnalytics {\n globalAnalytics = new TerseAnalytics(config);\n return globalAnalytics;\n}\n\n/**\n * Get global analytics instance\n */\nexport function getAnalytics(): TerseAnalytics | null {\n return globalAnalytics;\n}\n\n/**\n * Record an event to global analytics (if initialized)\n */\nexport function recordEvent(event: Omit<CompressionEvent, 'timestamp'>): void {\n globalAnalytics?.record(event);\n}\n\n/**\n * Quick setup for common use cases\n */\nexport const analytics = {\n /**\n * Enable local-only analytics (no cloud reporting)\n */\n local(options: { debug?: boolean; onEvent?: AnalyticsConfig['onEvent'] } = {}) {\n return initAnalytics({\n enabled: true,\n reportToCloud: false,\n debug: options.debug,\n onEvent: options.onEvent,\n });\n },\n\n /**\n * Enable cloud analytics with API key\n */\n cloud(apiKey: string, options: Partial<AnalyticsConfig> = {}) {\n return initAnalytics({\n ...options,\n enabled: true,\n reportToCloud: true,\n apiKey,\n });\n },\n\n /**\n * Get current stats\n */\n getStats() {\n return globalAnalytics?.getStats() ?? null;\n },\n\n /**\n * Get formatted summary\n */\n getSummary() {\n return globalAnalytics?.getSummary() ?? 'Analytics not initialized';\n },\n};\n\nexport default analytics;\n"]}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// src/analytics.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
enabled: false,
|
|
4
|
+
reportToCloud: false,
|
|
5
|
+
reportInterval: 6e4,
|
|
6
|
+
trackEndpoints: false,
|
|
7
|
+
endpoint: "https://api.tersejson.com/v1/analytics",
|
|
8
|
+
debug: false
|
|
9
|
+
};
|
|
10
|
+
var TerseAnalytics = class {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.events = [];
|
|
13
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
14
|
+
this.isNode = typeof window === "undefined";
|
|
15
|
+
this.stats = this.createEmptyStats();
|
|
16
|
+
if (this.config.enabled && this.config.reportToCloud) {
|
|
17
|
+
this.startReporting();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create empty stats object
|
|
22
|
+
*/
|
|
23
|
+
createEmptyStats() {
|
|
24
|
+
return {
|
|
25
|
+
totalEvents: 0,
|
|
26
|
+
totalOriginalBytes: 0,
|
|
27
|
+
totalCompressedBytes: 0,
|
|
28
|
+
totalBytesSaved: 0,
|
|
29
|
+
averageRatio: 0,
|
|
30
|
+
totalObjects: 0,
|
|
31
|
+
sessionStart: Date.now(),
|
|
32
|
+
lastEvent: Date.now()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record a compression event
|
|
37
|
+
*/
|
|
38
|
+
record(event) {
|
|
39
|
+
if (!this.config.enabled) return;
|
|
40
|
+
const fullEvent = {
|
|
41
|
+
...event,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
// Hash endpoint for privacy if tracking is enabled
|
|
44
|
+
endpoint: this.config.trackEndpoints && event.endpoint ? this.hashEndpoint(event.endpoint) : void 0
|
|
45
|
+
};
|
|
46
|
+
this.stats.totalEvents++;
|
|
47
|
+
this.stats.totalOriginalBytes += event.originalSize;
|
|
48
|
+
this.stats.totalCompressedBytes += event.compressedSize;
|
|
49
|
+
this.stats.totalBytesSaved += event.originalSize - event.compressedSize;
|
|
50
|
+
this.stats.totalObjects += event.objectCount;
|
|
51
|
+
this.stats.lastEvent = fullEvent.timestamp;
|
|
52
|
+
this.stats.averageRatio = this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;
|
|
53
|
+
this.events.push(fullEvent);
|
|
54
|
+
if (this.config.onEvent) {
|
|
55
|
+
this.config.onEvent(fullEvent);
|
|
56
|
+
}
|
|
57
|
+
if (this.config.debug) {
|
|
58
|
+
const savings = ((1 - event.compressedSize / event.originalSize) * 100).toFixed(1);
|
|
59
|
+
console.log(`[tersejson:analytics] ${event.originalSize} \u2192 ${event.compressedSize} bytes (${savings}% saved)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get current stats
|
|
64
|
+
*/
|
|
65
|
+
getStats() {
|
|
66
|
+
return { ...this.stats };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get formatted stats summary
|
|
70
|
+
*/
|
|
71
|
+
getSummary() {
|
|
72
|
+
const stats = this.stats;
|
|
73
|
+
const savedKB = (stats.totalBytesSaved / 1024).toFixed(2);
|
|
74
|
+
const savedPercent = stats.totalOriginalBytes > 0 ? ((1 - stats.averageRatio) * 100).toFixed(1) : "0";
|
|
75
|
+
return `TerseJSON Stats: ${stats.totalEvents} compressions, ${savedKB}KB saved (${savedPercent}% avg)`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Reset stats
|
|
79
|
+
*/
|
|
80
|
+
reset() {
|
|
81
|
+
this.events = [];
|
|
82
|
+
this.stats = this.createEmptyStats();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Start periodic reporting to cloud
|
|
86
|
+
*/
|
|
87
|
+
startReporting() {
|
|
88
|
+
if (this.reportTimer) return;
|
|
89
|
+
this.reportTimer = setInterval(() => {
|
|
90
|
+
this.reportToCloud();
|
|
91
|
+
}, this.config.reportInterval);
|
|
92
|
+
if (this.isNode && typeof process !== "undefined") {
|
|
93
|
+
process.on("beforeExit", () => this.reportToCloud());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stop reporting
|
|
98
|
+
*/
|
|
99
|
+
stop() {
|
|
100
|
+
if (this.reportTimer) {
|
|
101
|
+
clearInterval(this.reportTimer);
|
|
102
|
+
this.reportTimer = void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Report stats to tersejson.com
|
|
107
|
+
*/
|
|
108
|
+
async reportToCloud() {
|
|
109
|
+
if (!this.config.reportToCloud || this.events.length === 0) return;
|
|
110
|
+
const payload = {
|
|
111
|
+
apiKey: this.config.apiKey,
|
|
112
|
+
projectId: this.config.projectId,
|
|
113
|
+
stats: this.stats,
|
|
114
|
+
events: this.events.slice(-100),
|
|
115
|
+
// Last 100 events only
|
|
116
|
+
meta: {
|
|
117
|
+
version: "0.1.0",
|
|
118
|
+
runtime: this.isNode ? "node" : "browser"
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
const fetchFn = this.isNode ? (await import('https')).request : globalThis.fetch;
|
|
123
|
+
if (this.isNode) {
|
|
124
|
+
const url = new URL(this.config.endpoint);
|
|
125
|
+
const req = fetchFn({
|
|
126
|
+
hostname: url.hostname,
|
|
127
|
+
port: url.port || 443,
|
|
128
|
+
path: url.pathname,
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json"
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
req.write(JSON.stringify(payload));
|
|
135
|
+
req.end();
|
|
136
|
+
} else {
|
|
137
|
+
fetchFn(this.config.endpoint, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "Content-Type": "application/json" },
|
|
140
|
+
body: JSON.stringify(payload),
|
|
141
|
+
keepalive: true
|
|
142
|
+
// Allow sending on page unload
|
|
143
|
+
}).catch(() => {
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
this.events = [];
|
|
147
|
+
if (this.config.onStats) {
|
|
148
|
+
this.config.onStats(this.stats);
|
|
149
|
+
}
|
|
150
|
+
if (this.config.debug) {
|
|
151
|
+
console.log("[tersejson:analytics] Reported stats to cloud");
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
if (this.config.debug) {
|
|
155
|
+
console.log("[tersejson:analytics] Failed to report stats");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Hash endpoint for privacy
|
|
161
|
+
*/
|
|
162
|
+
hashEndpoint(endpoint) {
|
|
163
|
+
return endpoint.replace(/\/\d+/g, "/:id").replace(/\/[a-f0-9-]{36}/gi, "/:uuid").replace(/\?.*$/, "");
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var globalAnalytics = null;
|
|
167
|
+
function initAnalytics(config) {
|
|
168
|
+
globalAnalytics = new TerseAnalytics(config);
|
|
169
|
+
return globalAnalytics;
|
|
170
|
+
}
|
|
171
|
+
function getAnalytics() {
|
|
172
|
+
return globalAnalytics;
|
|
173
|
+
}
|
|
174
|
+
function recordEvent(event) {
|
|
175
|
+
globalAnalytics?.record(event);
|
|
176
|
+
}
|
|
177
|
+
var analytics = {
|
|
178
|
+
/**
|
|
179
|
+
* Enable local-only analytics (no cloud reporting)
|
|
180
|
+
*/
|
|
181
|
+
local(options = {}) {
|
|
182
|
+
return initAnalytics({
|
|
183
|
+
enabled: true,
|
|
184
|
+
reportToCloud: false,
|
|
185
|
+
debug: options.debug,
|
|
186
|
+
onEvent: options.onEvent
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Enable cloud analytics with API key
|
|
191
|
+
*/
|
|
192
|
+
cloud(apiKey, options = {}) {
|
|
193
|
+
return initAnalytics({
|
|
194
|
+
...options,
|
|
195
|
+
enabled: true,
|
|
196
|
+
reportToCloud: true,
|
|
197
|
+
apiKey
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Get current stats
|
|
202
|
+
*/
|
|
203
|
+
getStats() {
|
|
204
|
+
return globalAnalytics?.getStats() ?? null;
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* Get formatted summary
|
|
208
|
+
*/
|
|
209
|
+
getSummary() {
|
|
210
|
+
return globalAnalytics?.getSummary() ?? "Analytics not initialized";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var analytics_default = analytics;
|
|
214
|
+
|
|
215
|
+
export { TerseAnalytics, analytics, analytics_default as default, getAnalytics, initAnalytics, recordEvent };
|
|
216
|
+
//# sourceMappingURL=analytics.mjs.map
|
|
217
|
+
//# sourceMappingURL=analytics.mjs.map
|