app-tracker 3.0.0 → 3.1.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 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 createId;
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.createId();
61
+ }
59
62
  }
60
63
  addEventListener(apiKey) {
61
64
  if (apiKey != '' && !this.isNode && !this.isRunningLocal && !this.eventListenerAdded) {
@@ -76,7 +79,7 @@ class AppTrackerService {
76
79
  if (this.logs.length === 0)
77
80
  return;
78
81
  const logsToSend = this.logs.splice(0);
79
- const body = this.serializePayload(logsToSend, true);
82
+ const body = this.serializePayload(logsToSend, true, this.createId());
80
83
  const url = this.getLogsEndpoint();
81
84
  // sendBeacon is specifically designed to survive page termination. The API
82
85
  // key is also accepted in the request body by the server's ApiKeyGuard.
@@ -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,18 @@ 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 = {
178
+ eventId: this.createId(),
179
+ logType: type,
180
+ message: typeof message === 'string' ? log_privacy_1.sanitizeLogText(message) : message,
181
+ optionalParams: [],
182
+ createdAt: new Date()
183
+ };
184
184
  optionalParams.forEach((op) => {
185
185
  if (Array.isArray(op))
186
- op.forEach(o => log.optionalParams.push(o));
186
+ op.forEach(o => log.optionalParams.push(log_privacy_1.sanitizeLogValue(o)));
187
187
  else
188
- log.optionalParams.push(op);
188
+ log.optionalParams.push(log_privacy_1.sanitizeLogValue(op));
189
189
  });
190
190
  this.logs.push(log);
191
191
  const sendImmediately = type === log_1.LogType.critical || ((_a = this.sendLogsByTypesImmediately) === null || _a === void 0 ? void 0 : _a.includes(type)) === true;
@@ -238,11 +238,12 @@ class AppTrackerService {
238
238
  sendLogsToServerCore(logsToSend) {
239
239
  var _a;
240
240
  return __awaiter(this, void 0, void 0, function* () {
241
+ const batchId = this.createId();
241
242
  for (let sendTry = 1; sendTry <= this.maxSendTries; sendTry++) {
242
243
  const abortController = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
243
244
  const timeout = abortController ? setTimeout(() => abortController.abort(), this.requestTimeoutMs) : undefined;
244
245
  try {
245
- const body = this.serializePayload(logsToSend);
246
+ const body = this.serializePayload(logsToSend, false, batchId);
246
247
  const response = yield safeFetch(this.getLogsEndpoint(), {
247
248
  method: 'POST',
248
249
  headers: {
@@ -260,7 +261,7 @@ class AppTrackerService {
260
261
  return true;
261
262
  }
262
263
  catch (error) {
263
- console.error(`AppLogger: Could not send logs to server (attempt ${sendTry}/${this.maxSendTries})`, error);
264
+ console.error(`AppLogger: Could not send logs to server (attempt ${sendTry}/${this.maxSendTries})`);
264
265
  if (sendTry < this.maxSendTries)
265
266
  yield new Promise(resolve => setTimeout(resolve, this.retryDelayMs));
266
267
  }
@@ -277,15 +278,17 @@ class AppTrackerService {
277
278
  const last = this.endpointUrl.charAt(this.endpointUrl.length - 1);
278
279
  return `${this.endpointUrl}${last === '/' ? 'logs/createLog' : '/logs/createLog'}`;
279
280
  }
280
- serializePayload(logsToSend, includeApiKey = false) {
281
+ serializePayload(logsToSend, includeApiKey = false, batchId = this.createId()) {
281
282
  const seen = new WeakSet();
282
283
  const payload = {
283
- ident: this.ident,
284
284
  logs: logsToSend,
285
+ batchId: batchId,
285
286
  osType: this.isNode ? 'node' : 'web',
286
287
  packageVersion: version_1.APP_TRACKER_VERSION,
287
- userAgent: this.userAgent
288
+ clientInfo: this.clientInfo
288
289
  };
290
+ if (!this.isNode && this.correlationId)
291
+ payload.correlationId = this.correlationId;
289
292
  if (includeApiKey)
290
293
  payload.apiKey = this.apiKey;
291
294
  return JSON.stringify(payload, (_key, value) => {
@@ -307,26 +310,62 @@ class AppTrackerService {
307
310
  return value;
308
311
  });
309
312
  }
310
- setUserAgentFromOs() {
313
+ getBrowserClientInfo(userAgentOrOs) {
314
+ const userAgent = typeof userAgentOrOs === 'string' ? userAgentOrOs : '';
315
+ const browserPatterns = [
316
+ { name: 'Edge', expression: /Edg\/([0-9]+)/ },
317
+ { name: 'Firefox', expression: /Firefox\/([0-9]+)/ },
318
+ { name: 'Chrome', expression: /(?:Chrome|CriOS)\/([0-9]+)/ },
319
+ { name: 'Safari', expression: /Version\/([0-9]+).*Safari/ },
320
+ ];
321
+ const browser = browserPatterns
322
+ .map(item => ({ item, match: userAgent.match(item.expression) }))
323
+ .find(item => item.match !== null);
324
+ const android = userAgent.match(/Android\s+([0-9]+)/i);
325
+ const ios = userAgent.match(/(?:iPhone|iPad).*OS\s+([0-9]+)/i);
326
+ const windows = userAgent.match(/Windows NT\s+([0-9]+)/i);
327
+ const macOs = userAgent.match(/Mac OS X\s+([0-9]+)/i);
328
+ const os = android ? { name: 'Android', version: android[1], platform: '' }
329
+ : ios ? { name: 'iOS', version: ios[1], platform: '' }
330
+ : windows ? { name: 'Windows', version: windows[1], platform: '' }
331
+ : macOs ? { name: 'macOS', version: macOs[1], platform: '' }
332
+ : /Linux/i.test(userAgent) ? { name: 'Linux', version: '', platform: '' } : null;
333
+ return {
334
+ client: browser ? { type: 'browser', name: browser.item.name, version: browser.match[1] } : null,
335
+ os,
336
+ device: { type: /iPad|Tablet/i.test(userAgent) ? 'tablet' : /Mobile|Android|iPhone/i.test(userAgent) ? 'smartphone' : 'desktop' },
337
+ };
338
+ }
339
+ getNodeClientInfo(os) {
311
340
  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()
341
+ const call = (name) => os && typeof os[name] === 'function' ? String(os[name]()) : '';
342
+ return {
343
+ client: { type: 'library', name: 'Node.js', version: this.majorVersion((_a = this.nodeProcess) === null || _a === void 0 ? void 0 : _a.version) },
344
+ os: { name: call('platform') || ((_b = this.nodeProcess) === null || _b === void 0 ? void 0 : _b.platform) || '', version: this.majorVersion(call('release')), platform: call('arch') },
345
+ device: null,
322
346
  };
323
- return JSON.stringify(osInfo);
324
347
  }
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
- });
348
+ majorVersion(value) {
349
+ const match = typeof value === 'string' ? value.match(/\d+/) : null;
350
+ return match ? match[0] : '';
351
+ }
352
+ createId() {
353
+ var _a, _b;
354
+ const browserCrypto = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
355
+ if (typeof ((_a = browserCrypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function')
356
+ return browserCrypto.randomUUID();
357
+ const bytes = new Uint8Array(16);
358
+ if (typeof ((_b = browserCrypto) === null || _b === void 0 ? void 0 : _b.getRandomValues) === 'function') {
359
+ browserCrypto.getRandomValues(bytes);
360
+ }
361
+ else {
362
+ for (let index = 0; index < bytes.length; index++)
363
+ bytes[index] = Math.floor(Math.random() * 256);
364
+ }
365
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
366
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
367
+ const hex = Array.from(bytes).map(value => (`0${value.toString(16)}`).slice(-2)).join('');
368
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
330
369
  }
331
370
  }
332
371
  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,15 @@ 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 {
19
+ eventId: string;
23
20
  logType: LogType;
24
21
  message: any;
25
22
  optionalParams: any[];
26
23
  createdAt: Date;
27
- ident: string;
28
24
  }
29
25
  export declare enum LogType {
30
26
  debug = 0,
package/log.js CHANGED
@@ -13,10 +13,10 @@ class AppTrackerConfig {
13
13
  exports.AppTrackerConfig = AppTrackerConfig;
14
14
  class Log {
15
15
  constructor() {
16
+ this.eventId = '';
16
17
  this.logType = LogType.log;
17
18
  this.optionalParams = [];
18
19
  this.createdAt = new Date();
19
- this.ident = '';
20
20
  }
21
21
  }
22
22
  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.1",
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.1';