@tracelog/lib 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/browser/tracelog.esm.js +704 -607
- package/dist/browser/tracelog.esm.js.map +1 -0
- package/dist/browser/tracelog.js +2 -2
- package/dist/browser/tracelog.js.map +1 -0
- package/dist/cjs/api.js +12 -3
- package/dist/cjs/constants/config.constants.d.ts +3 -0
- package/dist/cjs/constants/config.constants.js +5 -2
- package/dist/cjs/managers/event.manager.d.ts +3 -0
- package/dist/cjs/managers/event.manager.js +47 -6
- package/dist/cjs/managers/storage.manager.d.ts +5 -0
- package/dist/cjs/managers/storage.manager.js +95 -6
- package/dist/cjs/types/config.types.d.ts +2 -2
- package/dist/cjs/utils/logging.utils.d.ts +16 -1
- package/dist/cjs/utils/logging.utils.js +65 -4
- package/dist/cjs/utils/network/url.utils.js +3 -4
- package/dist/cjs/utils/validations/config-validations.utils.js +17 -8
- package/dist/esm/api.js +12 -3
- package/dist/esm/constants/config.constants.d.ts +3 -0
- package/dist/esm/constants/config.constants.js +3 -0
- package/dist/esm/managers/event.manager.d.ts +3 -0
- package/dist/esm/managers/event.manager.js +48 -7
- package/dist/esm/managers/storage.manager.d.ts +5 -0
- package/dist/esm/managers/storage.manager.js +95 -6
- package/dist/esm/types/config.types.d.ts +2 -2
- package/dist/esm/utils/logging.utils.d.ts +16 -1
- package/dist/esm/utils/logging.utils.js +65 -4
- package/dist/esm/utils/network/url.utils.js +3 -4
- package/dist/esm/utils/validations/config-validations.utils.js +17 -8
- package/package.json +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EVENT_SENT_INTERVAL_MS, MAX_EVENTS_QUEUE_LENGTH, DUPLICATE_EVENT_THRESHOLD_MS, } from '../constants/config.constants';
|
|
1
|
+
import { EVENT_SENT_INTERVAL_MS, MAX_EVENTS_QUEUE_LENGTH, DUPLICATE_EVENT_THRESHOLD_MS, RATE_LIMIT_WINDOW_MS, MAX_EVENTS_PER_SECOND, MAX_PENDING_EVENTS_BUFFER, } from '../constants/config.constants';
|
|
2
2
|
import { EmitterEvent, EventType, Mode } from '../types';
|
|
3
3
|
import { getUTMParameters, log, generateEventId } from '../utils';
|
|
4
4
|
import { SenderManager } from './sender.manager';
|
|
@@ -11,6 +11,8 @@ export class EventManager extends StateManager {
|
|
|
11
11
|
this.lastEventFingerprint = null;
|
|
12
12
|
this.lastEventTime = 0;
|
|
13
13
|
this.sendIntervalId = null;
|
|
14
|
+
this.rateLimitCounter = 0;
|
|
15
|
+
this.rateLimitWindowStart = 0;
|
|
14
16
|
this.googleAnalytics = googleAnalytics;
|
|
15
17
|
this.dataSender = new SenderManager(storeManager);
|
|
16
18
|
this.emitter = emitter;
|
|
@@ -33,10 +35,19 @@ export class EventManager extends StateManager {
|
|
|
33
35
|
}
|
|
34
36
|
track({ type, page_url, from_page_url, scroll_data, click_data, custom_event, web_vitals, error_data, session_end_reason, }) {
|
|
35
37
|
if (!type) {
|
|
36
|
-
log('
|
|
38
|
+
log('error', 'Event type is required - event will be ignored');
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
41
|
+
// Check session BEFORE rate limiting to avoid consuming quota for buffered events
|
|
39
42
|
if (!this.get('sessionId')) {
|
|
43
|
+
// Protect against unbounded buffer growth during initialization delays
|
|
44
|
+
if (this.pendingEventsBuffer.length >= MAX_PENDING_EVENTS_BUFFER) {
|
|
45
|
+
// Drop oldest event (FIFO) to make room for new one
|
|
46
|
+
this.pendingEventsBuffer.shift();
|
|
47
|
+
log('warn', 'Pending events buffer full - dropping oldest event', {
|
|
48
|
+
data: { maxBufferSize: MAX_PENDING_EVENTS_BUFFER },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
40
51
|
this.pendingEventsBuffer.push({
|
|
41
52
|
type,
|
|
42
53
|
page_url,
|
|
@@ -50,10 +61,16 @@ export class EventManager extends StateManager {
|
|
|
50
61
|
});
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
64
|
+
// Rate limiting check (except for critical events)
|
|
65
|
+
// Applied AFTER session check to only rate-limit processable events
|
|
66
|
+
const isCriticalEvent = type === EventType.SESSION_START || type === EventType.SESSION_END;
|
|
67
|
+
if (!isCriticalEvent && !this.checkRateLimit()) {
|
|
68
|
+
// Rate limit exceeded - drop event silently
|
|
69
|
+
// Logging would itself cause performance issues
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
53
72
|
const eventType = type;
|
|
54
73
|
const isSessionStart = eventType === EventType.SESSION_START;
|
|
55
|
-
const isSessionEnd = eventType === EventType.SESSION_END;
|
|
56
|
-
const isCriticalEvent = isSessionStart || isSessionEnd;
|
|
57
74
|
const currentPageUrl = page_url || this.get('pageUrl');
|
|
58
75
|
const payload = this.buildEventPayload({
|
|
59
76
|
type: eventType,
|
|
@@ -72,7 +89,7 @@ export class EventManager extends StateManager {
|
|
|
72
89
|
if (isSessionStart) {
|
|
73
90
|
const currentSessionId = this.get('sessionId');
|
|
74
91
|
if (!currentSessionId) {
|
|
75
|
-
log('
|
|
92
|
+
log('error', 'Session start event requires sessionId - event will be ignored');
|
|
76
93
|
return;
|
|
77
94
|
}
|
|
78
95
|
if (this.get('hasStartSession')) {
|
|
@@ -105,6 +122,8 @@ export class EventManager extends StateManager {
|
|
|
105
122
|
this.pendingEventsBuffer = [];
|
|
106
123
|
this.lastEventFingerprint = null;
|
|
107
124
|
this.lastEventTime = 0;
|
|
125
|
+
this.rateLimitCounter = 0;
|
|
126
|
+
this.rateLimitWindowStart = 0;
|
|
108
127
|
this.dataSender.stop();
|
|
109
128
|
}
|
|
110
129
|
async flushImmediately() {
|
|
@@ -120,12 +139,19 @@ export class EventManager extends StateManager {
|
|
|
120
139
|
if (this.pendingEventsBuffer.length === 0) {
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
const currentSessionId = this.get('sessionId');
|
|
143
|
+
if (!currentSessionId) {
|
|
144
|
+
// Keep events in buffer for future retry - do NOT discard
|
|
145
|
+
// This prevents data loss during legitimate race conditions
|
|
146
|
+
log('warn', 'Cannot flush pending events: session not initialized - keeping in buffer', {
|
|
147
|
+
data: { bufferedEventCount: this.pendingEventsBuffer.length },
|
|
148
|
+
});
|
|
125
149
|
return;
|
|
126
150
|
}
|
|
151
|
+
// Create copy before clearing to avoid infinite recursion
|
|
127
152
|
const bufferedEvents = [...this.pendingEventsBuffer];
|
|
128
153
|
this.pendingEventsBuffer = [];
|
|
154
|
+
// Process all buffered events now that session exists
|
|
129
155
|
bufferedEvents.forEach((event) => {
|
|
130
156
|
this.track(event);
|
|
131
157
|
});
|
|
@@ -301,6 +327,21 @@ export class EventManager extends StateManager {
|
|
|
301
327
|
const samplingRate = this.get('config')?.samplingRate ?? 1;
|
|
302
328
|
return Math.random() < samplingRate;
|
|
303
329
|
}
|
|
330
|
+
checkRateLimit() {
|
|
331
|
+
const now = Date.now();
|
|
332
|
+
// Reset counter if window has expired
|
|
333
|
+
if (now - this.rateLimitWindowStart > RATE_LIMIT_WINDOW_MS) {
|
|
334
|
+
this.rateLimitCounter = 0;
|
|
335
|
+
this.rateLimitWindowStart = now;
|
|
336
|
+
}
|
|
337
|
+
// Check if limit exceeded
|
|
338
|
+
if (this.rateLimitCounter >= MAX_EVENTS_PER_SECOND) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
// Increment counter
|
|
342
|
+
this.rateLimitCounter++;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
304
345
|
removeProcessedEvents(eventIds) {
|
|
305
346
|
const eventIdSet = new Set(eventIds);
|
|
306
347
|
this.eventsQueue = this.eventsQueue.filter((event) => {
|
|
@@ -35,6 +35,11 @@ export declare class StorageManager {
|
|
|
35
35
|
* This indicates localStorage is full and data may not persist
|
|
36
36
|
*/
|
|
37
37
|
hasQuotaError(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
40
|
+
* Returns true if any data was removed, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
private cleanupOldData;
|
|
38
43
|
/**
|
|
39
44
|
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
40
45
|
*/
|
|
@@ -37,6 +37,9 @@ export class StorageManager {
|
|
|
37
37
|
* Stores an item in storage
|
|
38
38
|
*/
|
|
39
39
|
setItem(key, value) {
|
|
40
|
+
// Always update fallback FIRST for consistency
|
|
41
|
+
// This ensures fallback is in sync and can serve as backup if storage fails
|
|
42
|
+
this.fallbackStorage.set(key, value);
|
|
40
43
|
try {
|
|
41
44
|
if (this.storage) {
|
|
42
45
|
this.storage.setItem(key, value);
|
|
@@ -46,15 +49,37 @@ export class StorageManager {
|
|
|
46
49
|
catch (error) {
|
|
47
50
|
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
|
48
51
|
this.hasQuotaExceededError = true;
|
|
49
|
-
log('
|
|
50
|
-
error,
|
|
52
|
+
log('warn', 'localStorage quota exceeded, attempting cleanup', {
|
|
51
53
|
data: { key, valueSize: value.length },
|
|
52
54
|
});
|
|
55
|
+
// Attempt to free up space by removing old TraceLog data
|
|
56
|
+
const cleanedUp = this.cleanupOldData();
|
|
57
|
+
if (cleanedUp) {
|
|
58
|
+
// Retry after cleanup
|
|
59
|
+
try {
|
|
60
|
+
if (this.storage) {
|
|
61
|
+
this.storage.setItem(key, value);
|
|
62
|
+
// Successfully stored after cleanup
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (retryError) {
|
|
67
|
+
log('error', 'localStorage quota exceeded even after cleanup - data will not persist', {
|
|
68
|
+
error: retryError,
|
|
69
|
+
data: { key, valueSize: value.length },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
log('error', 'localStorage quota exceeded and no data to cleanup - data will not persist', {
|
|
75
|
+
error,
|
|
76
|
+
data: { key, valueSize: value.length },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
53
79
|
}
|
|
54
80
|
// Else: Silent fallback - user already warned in constructor
|
|
81
|
+
// Data is already in fallbackStorage (set at beginning)
|
|
55
82
|
}
|
|
56
|
-
// Always update fallback for consistency
|
|
57
|
-
this.fallbackStorage.set(key, value);
|
|
58
83
|
}
|
|
59
84
|
/**
|
|
60
85
|
* Removes an item from storage
|
|
@@ -108,6 +133,69 @@ export class StorageManager {
|
|
|
108
133
|
hasQuotaError() {
|
|
109
134
|
return this.hasQuotaExceededError;
|
|
110
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
138
|
+
* Returns true if any data was removed, false otherwise
|
|
139
|
+
*/
|
|
140
|
+
cleanupOldData() {
|
|
141
|
+
if (!this.storage) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const tracelogKeys = [];
|
|
146
|
+
const persistedEventsKeys = [];
|
|
147
|
+
// Collect all TraceLog keys
|
|
148
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
149
|
+
const key = this.storage.key(i);
|
|
150
|
+
if (key?.startsWith('tracelog_')) {
|
|
151
|
+
tracelogKeys.push(key);
|
|
152
|
+
// Prioritize removing old persisted events
|
|
153
|
+
if (key.startsWith('tracelog_persisted_events_')) {
|
|
154
|
+
persistedEventsKeys.push(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// First, try to remove old persisted events (usually the largest data)
|
|
159
|
+
if (persistedEventsKeys.length > 0) {
|
|
160
|
+
persistedEventsKeys.forEach((key) => {
|
|
161
|
+
try {
|
|
162
|
+
this.storage.removeItem(key);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Ignore errors during cleanup
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// Successfully cleaned up - no need to log in production
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
// If no persisted events, remove non-critical keys
|
|
172
|
+
// Define critical key prefixes that should be preserved
|
|
173
|
+
const criticalPrefixes = ['tracelog_session_', 'tracelog_user_id', 'tracelog_device_id', 'tracelog_config'];
|
|
174
|
+
const nonCriticalKeys = tracelogKeys.filter((key) => {
|
|
175
|
+
// Keep keys that start with any critical prefix
|
|
176
|
+
return !criticalPrefixes.some((prefix) => key.startsWith(prefix));
|
|
177
|
+
});
|
|
178
|
+
if (nonCriticalKeys.length > 0) {
|
|
179
|
+
// Remove up to 5 non-critical keys
|
|
180
|
+
const keysToRemove = nonCriticalKeys.slice(0, 5);
|
|
181
|
+
keysToRemove.forEach((key) => {
|
|
182
|
+
try {
|
|
183
|
+
this.storage.removeItem(key);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore errors during cleanup
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// Successfully cleaned up - no need to log in production
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
log('error', 'Failed to cleanup old data', { error });
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
111
199
|
/**
|
|
112
200
|
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
113
201
|
*/
|
|
@@ -145,6 +233,8 @@ export class StorageManager {
|
|
|
145
233
|
* Stores an item in sessionStorage
|
|
146
234
|
*/
|
|
147
235
|
setSessionItem(key, value) {
|
|
236
|
+
// Always update fallback FIRST for consistency
|
|
237
|
+
this.fallbackSessionStorage.set(key, value);
|
|
148
238
|
try {
|
|
149
239
|
if (this.sessionStorageRef) {
|
|
150
240
|
this.sessionStorageRef.setItem(key, value);
|
|
@@ -159,9 +249,8 @@ export class StorageManager {
|
|
|
159
249
|
});
|
|
160
250
|
}
|
|
161
251
|
// Else: Silent fallback - user already warned in constructor
|
|
252
|
+
// Data is already in fallbackSessionStorage (set at beginning)
|
|
162
253
|
}
|
|
163
|
-
// Always update fallback for consistency
|
|
164
|
-
this.fallbackSessionStorage.set(key, value);
|
|
165
254
|
}
|
|
166
255
|
/**
|
|
167
256
|
* Removes an item from sessionStorage
|
|
@@ -6,8 +6,6 @@ export interface Config {
|
|
|
6
6
|
globalMetadata?: Record<string, MetadataType>;
|
|
7
7
|
/** Selectors defining custom scroll containers to monitor. */
|
|
8
8
|
scrollContainerSelectors?: string | string[];
|
|
9
|
-
/** Enables HTTP requests for testing and development flows. */
|
|
10
|
-
allowHttp?: boolean;
|
|
11
9
|
/** Query parameters to remove before tracking URLs. */
|
|
12
10
|
sensitiveQueryParams?: string[];
|
|
13
11
|
/** Error event sampling rate between 0 and 1. */
|
|
@@ -25,6 +23,8 @@ export interface Config {
|
|
|
25
23
|
custom?: {
|
|
26
24
|
/** Required API URL for custom integration. */
|
|
27
25
|
apiUrl: string;
|
|
26
|
+
/** Allow HTTP URLs (not recommended for production). @default false */
|
|
27
|
+
allowHttp?: boolean;
|
|
28
28
|
};
|
|
29
29
|
/** Google Analytics integration options. */
|
|
30
30
|
googleAnalytics?: {
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
export declare const formatLogMsg: (msg: string, error?: unknown) => string;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Safe logging utility that respects production environment
|
|
4
|
+
*
|
|
5
|
+
* @param type - Log level (info, warn, error, debug)
|
|
6
|
+
* @param msg - Message to log
|
|
7
|
+
* @param extra - Optional extra data
|
|
8
|
+
*
|
|
9
|
+
* Production behavior:
|
|
10
|
+
* - debug: Never logged in production
|
|
11
|
+
* - info: Only logged if showToClient=true
|
|
12
|
+
* - warn: Always logged (important for debugging production issues)
|
|
13
|
+
* - error: Always logged
|
|
14
|
+
* - Stack traces are sanitized
|
|
15
|
+
* - Data objects are sanitized
|
|
16
|
+
*/
|
|
17
|
+
export declare const log: (type: "info" | "warn" | "error" | "debug", msg: string, extra?: {
|
|
3
18
|
error?: unknown;
|
|
4
19
|
data?: Record<string, unknown>;
|
|
5
20
|
showToClient?: boolean;
|
|
@@ -1,20 +1,81 @@
|
|
|
1
1
|
export const formatLogMsg = (msg, error) => {
|
|
2
2
|
if (error) {
|
|
3
|
+
// In production, sanitize error messages to avoid exposing sensitive paths
|
|
4
|
+
if (process.env.NODE_ENV !== 'dev' && error instanceof Error) {
|
|
5
|
+
// Remove file paths and line numbers from error messages
|
|
6
|
+
const sanitizedMessage = error.message.replace(/\s+at\s+.*$/gm, '').replace(/\(.*?:\d+:\d+\)/g, '');
|
|
7
|
+
return `[TraceLog] ${msg}: ${sanitizedMessage}`;
|
|
8
|
+
}
|
|
3
9
|
return `[TraceLog] ${msg}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
4
10
|
}
|
|
5
11
|
return `[TraceLog] ${msg}`;
|
|
6
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Safe logging utility that respects production environment
|
|
15
|
+
*
|
|
16
|
+
* @param type - Log level (info, warn, error, debug)
|
|
17
|
+
* @param msg - Message to log
|
|
18
|
+
* @param extra - Optional extra data
|
|
19
|
+
*
|
|
20
|
+
* Production behavior:
|
|
21
|
+
* - debug: Never logged in production
|
|
22
|
+
* - info: Only logged if showToClient=true
|
|
23
|
+
* - warn: Always logged (important for debugging production issues)
|
|
24
|
+
* - error: Always logged
|
|
25
|
+
* - Stack traces are sanitized
|
|
26
|
+
* - Data objects are sanitized
|
|
27
|
+
*/
|
|
7
28
|
export const log = (type, msg, extra) => {
|
|
8
|
-
const { error, data, showToClient } = extra ?? {};
|
|
29
|
+
const { error, data, showToClient = false } = extra ?? {};
|
|
9
30
|
const formattedMsg = error ? formatLogMsg(msg, error) : `[TraceLog] ${msg}`;
|
|
10
31
|
const method = type === 'error' ? 'error' : type === 'warn' ? 'warn' : 'log';
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
// Production logging strategy:
|
|
33
|
+
// - Development: Log everything
|
|
34
|
+
// - Production:
|
|
35
|
+
// - debug: never logged
|
|
36
|
+
// - info: only if showToClient=true
|
|
37
|
+
// - warn: always logged (critical for debugging)
|
|
38
|
+
// - error: always logged
|
|
39
|
+
const isProduction = process.env.NODE_ENV !== 'dev';
|
|
40
|
+
if (isProduction) {
|
|
41
|
+
// Never log debug in production
|
|
42
|
+
if (type === 'debug') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Log info only if explicitly flagged
|
|
46
|
+
if (type === 'info' && !showToClient) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// warn and error always logged in production
|
|
13
50
|
}
|
|
14
|
-
|
|
51
|
+
// In production, sanitize data to avoid exposing sensitive information
|
|
52
|
+
if (isProduction && data !== undefined) {
|
|
53
|
+
const sanitizedData = sanitizeLogData(data);
|
|
54
|
+
console[method](formattedMsg, sanitizedData);
|
|
55
|
+
}
|
|
56
|
+
else if (data !== undefined) {
|
|
15
57
|
console[method](formattedMsg, data);
|
|
16
58
|
}
|
|
17
59
|
else {
|
|
18
60
|
console[method](formattedMsg);
|
|
19
61
|
}
|
|
20
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Sanitizes log data in production to prevent sensitive information leakage
|
|
65
|
+
* Simple approach: redact sensitive keys only
|
|
66
|
+
*/
|
|
67
|
+
const sanitizeLogData = (data) => {
|
|
68
|
+
const sanitized = {};
|
|
69
|
+
const sensitiveKeys = ['token', 'password', 'secret', 'key', 'apikey', 'api_key', 'sessionid', 'session_id'];
|
|
70
|
+
for (const [key, value] of Object.entries(data)) {
|
|
71
|
+
const lowerKey = key.toLowerCase();
|
|
72
|
+
// Redact sensitive keys
|
|
73
|
+
if (sensitiveKeys.some((sensitiveKey) => lowerKey.includes(sensitiveKey))) {
|
|
74
|
+
sanitized[key] = '[REDACTED]';
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
sanitized[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sanitized;
|
|
81
|
+
};
|
|
@@ -22,7 +22,6 @@ const isValidUrl = (url, allowHttp = false) => {
|
|
|
22
22
|
* @returns The generated API URL
|
|
23
23
|
*/
|
|
24
24
|
export const getApiUrl = (config) => {
|
|
25
|
-
const allowHttp = config.allowHttp ?? false;
|
|
26
25
|
if (config.integrations?.tracelog?.projectId) {
|
|
27
26
|
const url = new URL(window.location.href);
|
|
28
27
|
const host = url.hostname;
|
|
@@ -32,9 +31,8 @@ export const getApiUrl = (config) => {
|
|
|
32
31
|
}
|
|
33
32
|
const projectId = config.integrations.tracelog.projectId;
|
|
34
33
|
const cleanDomain = parts.slice(-2).join('.');
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const isValid = isValidUrl(apiUrl, allowHttp);
|
|
34
|
+
const apiUrl = `https://${projectId}.${cleanDomain}`;
|
|
35
|
+
const isValid = isValidUrl(apiUrl);
|
|
38
36
|
if (!isValid) {
|
|
39
37
|
throw new Error('Invalid URL');
|
|
40
38
|
}
|
|
@@ -42,6 +40,7 @@ export const getApiUrl = (config) => {
|
|
|
42
40
|
}
|
|
43
41
|
if (config.integrations?.custom?.apiUrl) {
|
|
44
42
|
const apiUrl = config.integrations.custom.apiUrl;
|
|
43
|
+
const allowHttp = config.integrations?.custom?.allowHttp ?? false;
|
|
45
44
|
const isValid = isValidUrl(apiUrl, allowHttp);
|
|
46
45
|
if (!isValid) {
|
|
47
46
|
throw new Error('Invalid URL');
|
|
@@ -50,11 +50,6 @@ export const validateAppConfig = (config) => {
|
|
|
50
50
|
throw new SamplingRateValidationError(VALIDATION_MESSAGES.INVALID_SAMPLING_RATE, 'config');
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
if (config.allowHttp !== undefined) {
|
|
54
|
-
if (typeof config.allowHttp !== 'boolean') {
|
|
55
|
-
throw new AppConfigValidationError('allowHttp must be a boolean', 'config');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
53
|
};
|
|
59
54
|
/**
|
|
60
55
|
* Validates CSS selector syntax without executing querySelector (XSS prevention)
|
|
@@ -150,8 +145,16 @@ const validateIntegrations = (integrations) => {
|
|
|
150
145
|
integrations.custom.apiUrl.trim() === '') {
|
|
151
146
|
throw new IntegrationValidationError(VALIDATION_MESSAGES.INVALID_CUSTOM_API_URL, 'config');
|
|
152
147
|
}
|
|
153
|
-
if (
|
|
154
|
-
throw new IntegrationValidationError('
|
|
148
|
+
if (integrations.custom.allowHttp !== undefined && typeof integrations.custom.allowHttp !== 'boolean') {
|
|
149
|
+
throw new IntegrationValidationError('allowHttp must be a boolean', 'config');
|
|
150
|
+
}
|
|
151
|
+
const apiUrl = integrations.custom.apiUrl.trim();
|
|
152
|
+
if (!apiUrl.startsWith('http://') && !apiUrl.startsWith('https://')) {
|
|
153
|
+
throw new IntegrationValidationError('Custom API URL must start with "http://" or "https://"', 'config');
|
|
154
|
+
}
|
|
155
|
+
const allowHttp = integrations.custom.allowHttp ?? false;
|
|
156
|
+
if (!allowHttp && apiUrl.startsWith('http://')) {
|
|
157
|
+
throw new IntegrationValidationError('Custom API URL must use HTTPS in production. Set allowHttp: true in integration config to allow HTTP (not recommended)', 'config');
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
if (integrations.googleAnalytics) {
|
|
@@ -183,7 +186,13 @@ export const validateAndNormalizeConfig = (config) => {
|
|
|
183
186
|
sensitiveQueryParams: config.sensitiveQueryParams ?? [],
|
|
184
187
|
errorSampling: config.errorSampling ?? 1,
|
|
185
188
|
samplingRate: config.samplingRate ?? 1,
|
|
186
|
-
allowHttp: config.allowHttp ?? false,
|
|
187
189
|
};
|
|
190
|
+
// Normalize integrations
|
|
191
|
+
if (normalizedConfig.integrations?.custom) {
|
|
192
|
+
normalizedConfig.integrations.custom = {
|
|
193
|
+
...normalizedConfig.integrations.custom,
|
|
194
|
+
allowHttp: normalizedConfig.integrations.custom.allowHttp ?? false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
188
197
|
return normalizedConfig;
|
|
189
198
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tracelog/lib",
|
|
3
3
|
"description": "JavaScript library for web analytics and real-time event tracking",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.1",
|
|
6
6
|
"main": "./dist/cjs/public-api.js",
|
|
7
7
|
"module": "./dist/esm/public-api.js",
|
|
8
8
|
"types": "./dist/esm/public-api.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"serve": "http-server playground -p 3000 --cors",
|
|
44
44
|
"playground:setup": "npm run build:browser:dev && cp dist/browser/tracelog.esm.js playground/tracelog.js",
|
|
45
45
|
"playground:dev": "npm run playground:setup && npm run serve",
|
|
46
|
+
"playground:gh-pages": "npm run build:browser && cp dist/browser/tracelog.esm.js playground/tracelog.js",
|
|
46
47
|
"test:e2e": "npm run build:browser:dev && cp dist/browser/tracelog.esm.js playground/tracelog.js && NODE_ENV=dev playwright test",
|
|
47
48
|
"ci:build": "npm run build:all",
|
|
48
49
|
"prepare": "husky",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"changelog:preview": "node scripts/generate-changelog.js --dry-run"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"web-vitals": "
|
|
59
|
+
"web-vitals": "4.2.4"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@commitlint/config-conventional": "^19.8.1",
|