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.
@@ -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