app-tracker 3.0.0 → 3.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/README.md CHANGED
@@ -56,6 +56,7 @@ tracker.error("An error occurred", errorObj);
56
56
  - **Batching:** Logs are collected and sent after `maxLogsCount` or immediately for critical errors
57
57
  - **Explicit flush:** Call and await `tracker.flush()` before a controlled Node.js shutdown
58
58
  - **Environment Awareness:** Automatically adapts for browser/Node.js
59
+ - **Browser correlation:** Generates one in-memory correlation ID per page lifetime and includes it in every browser log batch. Node.js payloads do not include it. Use `tracker.getCorrelationId()` to display the current ID when needed.
59
60
  - **Secure Sending:** API key required, retry mechanism on errors
60
61
  - **External Dependency:** Uses `cross-fetch` for HTTP requests
61
62
 
@@ -11,14 +11,13 @@ export declare class AppTrackerService {
11
11
  private isSending;
12
12
  private activeSend?;
13
13
  private maxLogsCount;
14
- private userAgent;
14
+ private clientInfo;
15
+ private correlationId?;
15
16
  private logLevel;
16
17
  private sendLogsByTypesImmediately;
17
18
  private isNode;
18
19
  private nodeProcess;
19
- private os;
20
20
  private isRunningLocal;
21
- private ident;
22
21
  private eventListenerAdded;
23
22
  private _boundBeforeUnloadHandler?;
24
23
  init(apiKey: string, config?: AppTrackerConfig): void;
@@ -27,6 +26,7 @@ export declare class AppTrackerService {
27
26
  private beforeunloadHandler;
28
27
  private setRunningSystem;
29
28
  getIdent(): string;
29
+ getCorrelationId(): string;
30
30
  debug(message?: string, ...optionalParams: any[]): void;
31
31
  info(message?: string, ...optionalParams: any[]): void;
32
32
  log(message?: string, ...optionalParams: any[]): void;
@@ -43,6 +43,8 @@ export declare class AppTrackerService {
43
43
  private sendLogsToServerCore;
44
44
  private getLogsEndpoint;
45
45
  private serializePayload;
46
- private setUserAgentFromOs;
47
- private createUUID;
46
+ private getBrowserClientInfo;
47
+ private getNodeClientInfo;
48
+ private majorVersion;
49
+ private createCorrelationId;
48
50
  }
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const log_1 = require("./log");
13
13
  const cross_fetch_1 = require("cross-fetch");
14
14
  const version_1 = require("./version");
15
+ const log_privacy_1 = require("./log-privacy");
15
16
  const safeFetch = typeof window !== 'undefined' ? cross_fetch_1.default.bind(window) : cross_fetch_1.default;
16
17
  class AppTrackerService {
17
18
  constructor() {
@@ -25,12 +26,11 @@ class AppTrackerService {
25
26
  this.requestTimeoutMs = 10000;
26
27
  this.isSending = false;
27
28
  this.maxLogsCount = 10;
28
- this.userAgent = '';
29
+ this.clientInfo = null;
29
30
  this.logLevel = [log_1.LogType.error];
30
31
  this.sendLogsByTypesImmediately = [log_1.LogType.error, log_1.LogType.critical];
31
32
  this.isNode = false;
32
33
  this.isRunningLocal = true;
33
- this.ident = '';
34
34
  this.eventListenerAdded = false;
35
35
  }
36
36
  init(apiKey, config) {
@@ -56,6 +56,9 @@ class AppTrackerService {
56
56
  this.sendLogsByTypesImmediately = config.sendLogsByTypesImmediately;
57
57
  }
58
58
  this.setRunningSystem(config);
59
+ if (!this.isNode && !this.correlationId) {
60
+ this.correlationId = this.createCorrelationId();
61
+ }
59
62
  }
60
63
  addEventListener(apiKey) {
61
64
  if (apiKey != '' && !this.isNode && !this.isRunningLocal && !this.eventListenerAdded) {
@@ -92,35 +95,24 @@ class AppTrackerService {
92
95
  this.logs.unshift(...logsToSend);
93
96
  void this.flush();
94
97
  }
95
- setRunningSystem(config) {
98
+ setRunningSystem(_config) {
96
99
  var _a, _b, _c, _d, _e;
97
100
  this.nodeProcess = typeof globalThis !== 'undefined' ? globalThis.process : undefined;
98
101
  this.isNode = Object.prototype.toString.call(this.nodeProcess || 0) === '[object process]';
99
- if (!this.isNode) {
100
- this.userAgent = (_a = config) === null || _a === void 0 ? void 0 : _a.userAgentOrOs;
101
- this.ident = this.getIdent();
102
- }
103
- else {
104
- this.os = (_b = config) === null || _b === void 0 ? void 0 : _b.userAgentOrOs;
105
- this.userAgent = this.setUserAgentFromOs();
106
- }
102
+ this.clientInfo = this.isNode
103
+ ? this.getNodeClientInfo((_a = _config) === null || _a === void 0 ? void 0 : _a.userAgentOrOs)
104
+ : this.getBrowserClientInfo((_b = _config) === null || _b === void 0 ? void 0 : _b.userAgentOrOs);
107
105
  this.isRunningLocal = this.isNode
108
106
  ? (_e = (_d = (_c = this.nodeProcess) === null || _c === void 0 ? void 0 : _c.env) === null || _d === void 0 ? void 0 : _d.NODE_ENV, (_e !== null && _e !== void 0 ? _e : '')).indexOf("development") > -1
109
107
  : window.location.href.indexOf('localhost') > -1;
110
108
  }
111
109
  getIdent() {
112
- try {
113
- let ident = localStorage.getItem('App-Tracker.device-ident');
114
- if (!ident) {
115
- ident = this.createUUID();
116
- localStorage.setItem('App-Tracker.device-ident', ident);
117
- }
118
- return ident;
119
- }
120
- catch (_) {
121
- // Storage can be unavailable in sandboxed iframes or privacy modes.
122
- return this.createUUID();
123
- }
110
+ // Kept for API compatibility. Stable device identifiers are deliberately
111
+ // no longer created or stored because they allow cross-session tracking.
112
+ return '';
113
+ }
114
+ getCorrelationId() {
115
+ return this.isNode ? '' : this.correlationId || '';
124
116
  }
125
117
  debug(message, ...optionalParams) {
126
118
  this.addLog(log_1.LogType.debug, message, ...optionalParams);
@@ -155,22 +147,24 @@ class AppTrackerService {
155
147
  }
156
148
  logToConsole(type, message, ...optionalParams) {
157
149
  if (this.logInConsole) {
150
+ const safeMessage = typeof message === 'string' ? log_privacy_1.sanitizeLogText(message) : message;
151
+ const safeParams = optionalParams.map(param => log_privacy_1.sanitizeLogValue(param));
158
152
  switch (type) {
159
153
  case log_1.LogType.debug:
160
- console.debug(message, ...optionalParams);
154
+ console.debug(safeMessage, ...safeParams);
161
155
  break;
162
156
  case log_1.LogType.critical:
163
157
  case log_1.LogType.error:
164
- console.error(message, ...optionalParams);
158
+ console.error(safeMessage, ...safeParams);
165
159
  break;
166
160
  case log_1.LogType.info:
167
- console.info(message, ...optionalParams);
161
+ console.info(safeMessage, ...safeParams);
168
162
  break;
169
163
  case log_1.LogType.log:
170
- console.log(message, ...optionalParams);
164
+ console.log(safeMessage, ...safeParams);
171
165
  break;
172
166
  case log_1.LogType.warn:
173
- console.warn(message, ...optionalParams);
167
+ console.warn(safeMessage, ...safeParams);
174
168
  break;
175
169
  default:
176
170
  console.warn("Unkown LogType: ", type);
@@ -180,12 +174,12 @@ class AppTrackerService {
180
174
  }
181
175
  sendLogsToServer(type, message, ...optionalParams) {
182
176
  var _a, _b;
183
- const log = { logType: type, message: message, optionalParams: [], createdAt: new Date(), ident: this.ident };
177
+ const log = { logType: type, message: typeof message === 'string' ? log_privacy_1.sanitizeLogText(message) : message, optionalParams: [], createdAt: new Date() };
184
178
  optionalParams.forEach((op) => {
185
179
  if (Array.isArray(op))
186
- op.forEach(o => log.optionalParams.push(o));
180
+ op.forEach(o => log.optionalParams.push(log_privacy_1.sanitizeLogValue(o)));
187
181
  else
188
- log.optionalParams.push(op);
182
+ log.optionalParams.push(log_privacy_1.sanitizeLogValue(op));
189
183
  });
190
184
  this.logs.push(log);
191
185
  const sendImmediately = type === log_1.LogType.critical || ((_a = this.sendLogsByTypesImmediately) === null || _a === void 0 ? void 0 : _a.includes(type)) === true;
@@ -260,7 +254,7 @@ class AppTrackerService {
260
254
  return true;
261
255
  }
262
256
  catch (error) {
263
- console.error(`AppLogger: Could not send logs to server (attempt ${sendTry}/${this.maxSendTries})`, error);
257
+ console.error(`AppLogger: Could not send logs to server (attempt ${sendTry}/${this.maxSendTries})`);
264
258
  if (sendTry < this.maxSendTries)
265
259
  yield new Promise(resolve => setTimeout(resolve, this.retryDelayMs));
266
260
  }
@@ -280,12 +274,13 @@ class AppTrackerService {
280
274
  serializePayload(logsToSend, includeApiKey = false) {
281
275
  const seen = new WeakSet();
282
276
  const payload = {
283
- ident: this.ident,
284
277
  logs: logsToSend,
285
278
  osType: this.isNode ? 'node' : 'web',
286
279
  packageVersion: version_1.APP_TRACKER_VERSION,
287
- userAgent: this.userAgent
280
+ clientInfo: this.clientInfo
288
281
  };
282
+ if (!this.isNode && this.correlationId)
283
+ payload.correlationId = this.correlationId;
289
284
  if (includeApiKey)
290
285
  payload.apiKey = this.apiKey;
291
286
  return JSON.stringify(payload, (_key, value) => {
@@ -307,26 +302,62 @@ class AppTrackerService {
307
302
  return value;
308
303
  });
309
304
  }
310
- setUserAgentFromOs() {
305
+ getBrowserClientInfo(userAgentOrOs) {
306
+ const userAgent = typeof userAgentOrOs === 'string' ? userAgentOrOs : '';
307
+ const browserPatterns = [
308
+ { name: 'Edge', expression: /Edg\/([0-9]+)/ },
309
+ { name: 'Firefox', expression: /Firefox\/([0-9]+)/ },
310
+ { name: 'Chrome', expression: /(?:Chrome|CriOS)\/([0-9]+)/ },
311
+ { name: 'Safari', expression: /Version\/([0-9]+).*Safari/ },
312
+ ];
313
+ const browser = browserPatterns
314
+ .map(item => ({ item, match: userAgent.match(item.expression) }))
315
+ .find(item => item.match !== null);
316
+ const android = userAgent.match(/Android\s+([0-9]+)/i);
317
+ const ios = userAgent.match(/(?:iPhone|iPad).*OS\s+([0-9]+)/i);
318
+ const windows = userAgent.match(/Windows NT\s+([0-9]+)/i);
319
+ const macOs = userAgent.match(/Mac OS X\s+([0-9]+)/i);
320
+ const os = android ? { name: 'Android', version: android[1], platform: '' }
321
+ : ios ? { name: 'iOS', version: ios[1], platform: '' }
322
+ : windows ? { name: 'Windows', version: windows[1], platform: '' }
323
+ : macOs ? { name: 'macOS', version: macOs[1], platform: '' }
324
+ : /Linux/i.test(userAgent) ? { name: 'Linux', version: '', platform: '' } : null;
325
+ return {
326
+ client: browser ? { type: 'browser', name: browser.item.name, version: browser.match[1] } : null,
327
+ os,
328
+ device: { type: /iPad|Tablet/i.test(userAgent) ? 'tablet' : /Mobile|Android|iPhone/i.test(userAgent) ? 'smartphone' : 'desktop' },
329
+ };
330
+ }
331
+ getNodeClientInfo(os) {
311
332
  var _a, _b;
312
- if (!this.os) {
313
- return JSON.stringify({ platform: (_a = this.nodeProcess) === null || _a === void 0 ? void 0 : _a.platform, version: (_b = this.nodeProcess) === null || _b === void 0 ? void 0 : _b.version });
314
- }
315
- const osInfo = {
316
- arch: this.os.arch(),
317
- platform: this.os.platform(),
318
- release: this.os.release(),
319
- type: this.os.type(),
320
- version: this.os.version(),
321
- hostname: this.os.hostname()
333
+ const call = (name) => os && typeof os[name] === 'function' ? String(os[name]()) : '';
334
+ return {
335
+ client: { type: 'library', name: 'Node.js', version: this.majorVersion((_a = this.nodeProcess) === null || _a === void 0 ? void 0 : _a.version) },
336
+ os: { name: call('platform') || ((_b = this.nodeProcess) === null || _b === void 0 ? void 0 : _b.platform) || '', version: this.majorVersion(call('release')), platform: call('arch') },
337
+ device: null,
322
338
  };
323
- return JSON.stringify(osInfo);
324
339
  }
325
- createUUID() {
326
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
327
- const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
328
- return v.toString(16);
329
- });
340
+ majorVersion(value) {
341
+ const match = typeof value === 'string' ? value.match(/\d+/) : null;
342
+ return match ? match[0] : '';
343
+ }
344
+ createCorrelationId() {
345
+ var _a, _b;
346
+ const browserCrypto = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
347
+ if (typeof ((_a = browserCrypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function')
348
+ return browserCrypto.randomUUID();
349
+ const bytes = new Uint8Array(16);
350
+ if (typeof ((_b = browserCrypto) === null || _b === void 0 ? void 0 : _b.getRandomValues) === 'function') {
351
+ browserCrypto.getRandomValues(bytes);
352
+ }
353
+ else {
354
+ for (let index = 0; index < bytes.length; index++)
355
+ bytes[index] = Math.floor(Math.random() * 256);
356
+ }
357
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
358
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
359
+ const hex = Array.from(bytes).map(value => (`0${value.toString(16)}`).slice(-2)).join('');
360
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
330
361
  }
331
362
  }
332
363
  exports.AppTrackerService = AppTrackerService;
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeLogText(value: string): string;
2
+ export declare function sanitizeLogValue(value: any, depth?: number, seen?: WeakSet<object>): any;
package/log-privacy.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const REDACTED = '[REDACTED]';
4
+ const SENSITIVE_KEYS = new Set(['authorization', 'cookie', 'password', 'passwd', 'secret', 'token', 'apikey', 'email', 'emailaddress', 'phone', 'mobile', 'firstname', 'lastname', 'fullname', 'address', 'street', 'postcode', 'postalcode', 'ip', 'ipaddress', 'hostname', 'ident', 'userid', 'customerid', 'paymentid', 'sessionid', 'refreshtoken', 'accesstoken', 'recipient', 'recipients', 'username', 'displayname', 'birthdate', 'dateofbirth', 'dob', 'iban', 'bic', 'accountnumber', 'creditcard', 'cardnumber', 'coordinates', 'latitude', 'longitude']);
5
+ const SECRET_KEY_PARTS = ['authorization', 'cookie', 'password', 'passwd', 'secret', 'token', 'apikey', 'credential', 'privatekey'];
6
+ function isSensitiveKey(key) {
7
+ const normalizedKey = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
8
+ return SENSITIVE_KEYS.has(normalizedKey) || SECRET_KEY_PARTS.some(part => normalizedKey.indexOf(part) >= 0);
9
+ }
10
+ function sanitizeLogUrl(value) {
11
+ let sanitizedUrl = '[REDACTED URL]';
12
+ try {
13
+ const url = new URL(value);
14
+ const identityRoutes = new Set(['users', 'customers', 'accounts', 'sessions', 'payments']);
15
+ const segments = url.pathname.split('/').map((segment, index, allSegments) => {
16
+ const followsIdentityRoute = index > 0 && identityRoutes.has(allSegments[index - 1].toLowerCase());
17
+ const isIdentifier = /^[0-9a-f]{24}$/i.test(segment) || /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(segment) || /^(?:cus|pi|pm|seti|sub|cs|price|in|ch)_[A-Za-z0-9_]+$/.test(segment) || /^\d{6,}$/.test(segment);
18
+ return followsIdentityRoute || isIdentifier ? ':id' : segment;
19
+ });
20
+ sanitizedUrl = `${url.protocol}//${url.host}${segments.join('/')}`;
21
+ }
22
+ catch (_error) {
23
+ // Keep the privacy-safe fallback.
24
+ }
25
+ return sanitizedUrl;
26
+ }
27
+ function sanitizeLogText(value) {
28
+ return value
29
+ .replace(/https?:\/\/[^\s"'<>]+/gi, (url) => sanitizeLogUrl(url))
30
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, REDACTED)
31
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, REDACTED)
32
+ .replace(/\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g, REDACTED)
33
+ .replace(/\+\d[\d\s().-]{7,}\d/g, REDACTED)
34
+ .replace(/[A-Z]:\\Users\\[^\\\s]+/gi, REDACTED)
35
+ .replace(/\/home\/[^/\s]+/g, REDACTED)
36
+ .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+=*/gi, REDACTED)
37
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED);
38
+ }
39
+ exports.sanitizeLogText = sanitizeLogText;
40
+ function sanitizeLogValue(value, depth = 0, seen = new WeakSet()) {
41
+ if (typeof value === 'string')
42
+ return sanitizeLogText(value);
43
+ if (value === null || typeof value === 'undefined' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
44
+ return value;
45
+ if (depth >= 6)
46
+ return '[TRUNCATED]';
47
+ if (value instanceof Date)
48
+ return value.toISOString();
49
+ if (value instanceof Error)
50
+ return Object.assign({ name: sanitizeLogText(value.name), message: sanitizeLogText(value.message) }, (typeof value.stack === 'string' ? { stack: sanitizeLogText(value.stack) } : {}));
51
+ if (typeof value === 'object') {
52
+ if (seen.has(value))
53
+ return '[Circular]';
54
+ seen.add(value);
55
+ if (Array.isArray(value))
56
+ return value.map(item => sanitizeLogValue(item, depth + 1, seen));
57
+ const result = {};
58
+ Object.keys(value).forEach(key => result[key] = isSensitiveKey(key) ? REDACTED : sanitizeLogValue(value[key], depth + 1, seen));
59
+ return result;
60
+ }
61
+ return `[${typeof value}]`;
62
+ }
63
+ exports.sanitizeLogValue = sanitizeLogValue;
package/log.d.ts CHANGED
@@ -12,19 +12,14 @@ export declare class AppTrackerConfig {
12
12
  /** Defines which log types should be sent to the server */
13
13
  logLevel?: LogType[];
14
14
  sendLogsByTypesImmediately?: LogType[];
15
- /**
16
- * Please pass:
17
- * Web -> navigator.userAgent,
18
- * Node -> os (from require('os'))
19
- */
20
- userAgentOrOs: any;
15
+ /** Used only to derive coarse browser/OS categories; raw values are not transmitted. */
16
+ userAgentOrOs?: any;
21
17
  }
22
18
  export declare class Log {
23
19
  logType: LogType;
24
20
  message: any;
25
21
  optionalParams: any[];
26
22
  createdAt: Date;
27
- ident: string;
28
23
  }
29
24
  export declare enum LogType {
30
25
  debug = 0,
package/log.js CHANGED
@@ -16,7 +16,6 @@ class Log {
16
16
  this.logType = LogType.log;
17
17
  this.optionalParams = [];
18
18
  this.createdAt = new Date();
19
- this.ident = '';
20
19
  }
21
20
  }
22
21
  exports.Log = Log;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tracker",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "App Tracker for Web-Applications",
5
5
  "main": "index.js",
6
6
  "repository": {
package/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /** Replaced with the package.json version by the build script. */
4
- exports.APP_TRACKER_VERSION = '3.0.0';
4
+ exports.APP_TRACKER_VERSION = '3.1.0';