@logbrew/sdk 0.1.38 → 0.1.39
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/automatic-delivery.cjs +271 -0
- package/batch-sender.cjs +164 -0
- package/bounded-number.cjs +34 -0
- package/browser-breadcrumbs.cjs +346 -136
- package/capture-support.cjs +100 -0
- package/client-options.cjs +292 -0
- package/client-report.cjs +232 -0
- package/client.cjs +447 -0
- package/console-arguments.cjs +151 -0
- package/console-capture.cjs +194 -0
- package/core.cjs +30 -2810
- package/delivery-health.cjs +133 -0
- package/error-causes.cjs +130 -0
- package/error-details.cjs +124 -0
- package/error-issue.cjs +187 -0
- package/event-clone.cjs +55 -0
- package/event-metadata.cjs +40 -0
- package/event-pipeline.cjs +325 -0
- package/event-queue.cjs +306 -0
- package/event-store.cjs +237 -0
- package/event-validation.cjs +159 -0
- package/examples/agent-timeline.cjs +57 -46
- package/examples/agent-timeline.mjs +41 -35
- package/examples/index.mjs +122 -94
- package/examples/readme-example.cjs +36 -27
- package/examples/readme-example.mjs +21 -17
- package/examples/real-user-smoke.cjs +41 -32
- package/examples/real-user-smoke.mjs +26 -22
- package/exception-chain.cjs +205 -0
- package/index.d.ts +144 -1076
- package/index.js +1 -1
- package/issue-exception.cjs +13 -8
- package/metric-validation.cjs +95 -0
- package/network-milestone.cjs +155 -0
- package/null-value.cjs +4 -0
- package/opentelemetry-bridge.cjs +330 -0
- package/opentelemetry-context.cjs +245 -0
- package/opentelemetry-exceptions.cjs +69 -0
- package/opentelemetry-keys.cjs +183 -0
- package/opentelemetry-span.cjs +286 -0
- package/opentelemetry-summary.cjs +213 -0
- package/opentelemetry-time.cjs +131 -0
- package/opentelemetry.cjs +99 -1068
- package/package.json +55 -1
- package/pino-destination.cjs +108 -0
- package/pino-record.cjs +237 -0
- package/product-action.cjs +163 -0
- package/provided-breadcrumbs.cjs +117 -0
- package/random-hex.cjs +29 -0
- package/react-native.js +1 -1
- package/recording-transport.cjs +68 -0
- package/release-artifacts-build.cjs +263 -84
- package/release-artifacts-common.js +347 -66
- package/release-artifacts-debug-id.js +178 -0
- package/release-artifacts-inspect.js +213 -0
- package/release-artifacts-issue-event.js +99 -0
- package/release-artifacts-manifest-artifact.js +107 -0
- package/release-artifacts-manifest.js +77 -0
- package/release-artifacts-mappings.js +265 -0
- package/release-artifacts-prepare.js +297 -0
- package/release-artifacts-source-context.js +210 -0
- package/release-artifacts-symbolication.js +272 -421
- package/release-artifacts-upload-files.js +122 -0
- package/release-artifacts-upload-request.js +246 -0
- package/release-artifacts-upload.js +155 -300
- package/release-artifacts-urls.js +49 -0
- package/release-artifacts.js +189 -662
- package/retry-delay.cjs +32 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/types/client.d.ts +67 -0
- package/types/delivery.d.ts +125 -0
- package/types/issues.d.ts +249 -0
- package/types/logging.d.ts +175 -0
- package/types/opentelemetry.d.ts +205 -0
- package/types/product.d.ts +128 -0
- package/types/recording-transport.d.ts +15 -0
- package/types/sdk-error.d.ts +9 -0
- package/types/telemetry.d.ts +171 -0
- package/types/trace-context.d.ts +110 -0
- package/types/transport-error.d.ts +10 -0
- package/utf8.cjs +37 -0
- package/validation.cjs +49 -1
- package/vite-release-artifacts-config.cjs +120 -0
- package/vite-release-artifacts.cjs +199 -99
- package/vite-release-artifacts.d.cts +65 -40
- package/vite-release-artifacts.d.ts +65 -40
- package/vite-release-artifacts.js +2 -2
- package/winston-info.cjs +205 -0
- package/winston.cjs +189 -289
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -45,9 +45,43 @@
|
|
|
45
45
|
"index.cjs",
|
|
46
46
|
"browser-breadcrumbs.cjs",
|
|
47
47
|
"core.cjs",
|
|
48
|
+
"capture-support.cjs",
|
|
49
|
+
"client.cjs",
|
|
50
|
+
"automatic-delivery.cjs",
|
|
51
|
+
"batch-sender.cjs",
|
|
52
|
+
"bounded-number.cjs",
|
|
53
|
+
"client-options.cjs",
|
|
54
|
+
"client-report.cjs",
|
|
55
|
+
"delivery-health.cjs",
|
|
56
|
+
"event-pipeline.cjs",
|
|
57
|
+
"event-queue.cjs",
|
|
58
|
+
"event-store.cjs",
|
|
59
|
+
"provided-breadcrumbs.cjs",
|
|
60
|
+
"random-hex.cjs",
|
|
61
|
+
"retry-delay.cjs",
|
|
62
|
+
"stored-events.cjs",
|
|
63
|
+
"utf8.cjs",
|
|
64
|
+
"console-arguments.cjs",
|
|
65
|
+
"console-capture.cjs",
|
|
66
|
+
"error-causes.cjs",
|
|
67
|
+
"error-details.cjs",
|
|
68
|
+
"error-issue.cjs",
|
|
69
|
+
"event-clone.cjs",
|
|
70
|
+
"event-metadata.cjs",
|
|
71
|
+
"event-validation.cjs",
|
|
72
|
+
"exception-chain.cjs",
|
|
73
|
+
"metric-validation.cjs",
|
|
74
|
+
"network-milestone.cjs",
|
|
75
|
+
"null-value.cjs",
|
|
76
|
+
"pino-destination.cjs",
|
|
77
|
+
"pino-record.cjs",
|
|
78
|
+
"product-action.cjs",
|
|
79
|
+
"recording-transport.cjs",
|
|
80
|
+
"span-validation.cjs",
|
|
48
81
|
"index.js",
|
|
49
82
|
"index.d.ts",
|
|
50
83
|
"index.d.cts",
|
|
84
|
+
"types",
|
|
51
85
|
"issue-breadcrumbs.cjs",
|
|
52
86
|
"issue-diagnostics.cjs",
|
|
53
87
|
"issue-evidence.cjs",
|
|
@@ -58,6 +92,13 @@
|
|
|
58
92
|
"issue-text.cjs",
|
|
59
93
|
"log-context.cjs",
|
|
60
94
|
"opentelemetry.cjs",
|
|
95
|
+
"opentelemetry-bridge.cjs",
|
|
96
|
+
"opentelemetry-context.cjs",
|
|
97
|
+
"opentelemetry-exceptions.cjs",
|
|
98
|
+
"opentelemetry-keys.cjs",
|
|
99
|
+
"opentelemetry-span.cjs",
|
|
100
|
+
"opentelemetry-summary.cjs",
|
|
101
|
+
"opentelemetry-time.cjs",
|
|
61
102
|
"react-native.js",
|
|
62
103
|
"react-native.d.ts",
|
|
63
104
|
"sdk-error.cjs",
|
|
@@ -67,12 +108,25 @@
|
|
|
67
108
|
"transport-error.cjs",
|
|
68
109
|
"validation.cjs",
|
|
69
110
|
"winston.cjs",
|
|
111
|
+
"winston-info.cjs",
|
|
70
112
|
"release-artifacts-common.js",
|
|
71
113
|
"release-artifacts-build.cjs",
|
|
72
114
|
"release-artifacts.js",
|
|
73
115
|
"release-artifacts-upload.js",
|
|
74
116
|
"release-artifacts-symbolication.js",
|
|
117
|
+
"release-artifacts-debug-id.js",
|
|
118
|
+
"release-artifacts-inspect.js",
|
|
119
|
+
"release-artifacts-issue-event.js",
|
|
120
|
+
"release-artifacts-manifest.js",
|
|
121
|
+
"release-artifacts-manifest-artifact.js",
|
|
122
|
+
"release-artifacts-mappings.js",
|
|
123
|
+
"release-artifacts-prepare.js",
|
|
124
|
+
"release-artifacts-source-context.js",
|
|
125
|
+
"release-artifacts-upload-files.js",
|
|
126
|
+
"release-artifacts-upload-request.js",
|
|
127
|
+
"release-artifacts-urls.js",
|
|
75
128
|
"vite-release-artifacts.cjs",
|
|
129
|
+
"vite-release-artifacts-config.cjs",
|
|
76
130
|
"vite-release-artifacts.js",
|
|
77
131
|
"vite-release-artifacts.d.ts",
|
|
78
132
|
"vite-release-artifacts.d.cts",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const { callableOrUndefined, captureSettings, settledFlush } = require("./capture-support.cjs");
|
|
2
|
+
const { LogBrewClient } = require("./client.cjs");
|
|
3
|
+
const { traceFromProvider } = require("./log-context.cjs");
|
|
4
|
+
const { NULL_VALUE } = require("./null-value.cjs");
|
|
5
|
+
const { logAttributesFromPinoRecord, timestampFromPinoRecord } = require("./pino-record.cjs");
|
|
6
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
7
|
+
const { isObjectValue, isTruthy } = require("./validation.cjs");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @import { CaptureClient } from "./capture-support.cjs"
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* write: (chunk: unknown) => boolean,
|
|
13
|
+
* flush: () => Promise<unknown>,
|
|
14
|
+
* end: () => Promise<unknown>
|
|
15
|
+
* }} PinoDestination
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const NO_PENDING_EVENTS = 0;
|
|
19
|
+
const LINE_BREAK_PATTERN = /\r?\n/u;
|
|
20
|
+
|
|
21
|
+
class PinoDestinationState {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Readonly<Record<string, unknown>>} config
|
|
24
|
+
* @param {CaptureClient} client
|
|
25
|
+
*/
|
|
26
|
+
constructor(config, client) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.settings = captureSettings(config, "flushOnWrite", "pino");
|
|
29
|
+
this.traceProvider = callableOrUndefined(config["traceProvider"]);
|
|
30
|
+
this.captured = 0;
|
|
31
|
+
/** @type {Promise<unknown>} */
|
|
32
|
+
this.pendingFlush = Promise.resolve(NULL_VALUE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} line
|
|
37
|
+
* @returns {void}
|
|
38
|
+
*/
|
|
39
|
+
writeLine(line) {
|
|
40
|
+
const { settings } = this;
|
|
41
|
+
try {
|
|
42
|
+
/** @type {unknown} */
|
|
43
|
+
const record = JSON.parse(line);
|
|
44
|
+
this.captured += 1;
|
|
45
|
+
this.client.log(
|
|
46
|
+
`${String(settings.eventIdPrefix)}_${this.captured}`,
|
|
47
|
+
timestampFromPinoRecord(record, settings.timestamp),
|
|
48
|
+
logAttributesFromPinoRecord(record, {
|
|
49
|
+
includeErrorStack: settings.includeErrorStack,
|
|
50
|
+
logger: settings.logger,
|
|
51
|
+
metadata: settings.metadata,
|
|
52
|
+
trace: traceFromProvider(this.traceProvider, settings.onError)
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
if (settings.flushOnEvent && isTruthy(settings.transport)) {
|
|
56
|
+
this.pendingFlush = settledFlush(this.client.flush(settings.transport), settings.onError);
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
settings.onError(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
flushPending() {
|
|
67
|
+
const { settings } = this;
|
|
68
|
+
if (isTruthy(settings.transport) && this.client.pendingEvents() > NO_PENDING_EVENTS) {
|
|
69
|
+
this.pendingFlush = settledFlush(this.client.flush(settings.transport), settings.onError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {unknown} config
|
|
76
|
+
* @returns {PinoDestination}
|
|
77
|
+
*/
|
|
78
|
+
function createLogBrewPinoDestination(config) {
|
|
79
|
+
if (!isObjectValue(config)) {
|
|
80
|
+
throw new SdkError("validation_error", "Pino destination config must be an object");
|
|
81
|
+
}
|
|
82
|
+
const { client } = config;
|
|
83
|
+
if (!(client instanceof LogBrewClient)) {
|
|
84
|
+
throw new SdkError("validation_error", "Pino destination client must be a LogBrewClient");
|
|
85
|
+
}
|
|
86
|
+
const state = new PinoDestinationState(config, client);
|
|
87
|
+
return {
|
|
88
|
+
write(chunk) {
|
|
89
|
+
for (const line of String(chunk).split(LINE_BREAK_PATTERN)) {
|
|
90
|
+
if (line.trim() !== "") {
|
|
91
|
+
state.writeLine(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async flush() {
|
|
98
|
+
state.flushPending();
|
|
99
|
+
return state.pendingFlush;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async end() {
|
|
103
|
+
return this.flush();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { createLogBrewPinoDestination };
|
package/pino-record.cjs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const { compactMetadata, isMetadataValue, traceMetadataFromLogContext } = require("./log-context.cjs");
|
|
2
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
3
|
+
const { isObjectValue, isRecord, isTruthy, stringOrUndefined } = require("./validation.cjs");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @import { Severity } from "./index.cjs"
|
|
7
|
+
* @typedef {Readonly<{ includeErrorStack?: unknown, logger?: unknown, metadata?: unknown, trace?: unknown }>} PinoRecordOptions
|
|
8
|
+
* @typedef {{ message: string, level: Severity, logger?: unknown, metadata: Record<string, unknown> }} PinoLogAttributes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const CRITICAL_LEVEL = 60;
|
|
12
|
+
const ERROR_LEVEL = 50;
|
|
13
|
+
const WARNING_LEVEL = 40;
|
|
14
|
+
const TRACE_LEVEL = 10;
|
|
15
|
+
const DEBUG_LEVEL = 20;
|
|
16
|
+
const INFO_LEVEL = 30;
|
|
17
|
+
/** @type {ReadonlyMap<number, string>} */
|
|
18
|
+
const PINO_LEVEL_LABELS = new Map([
|
|
19
|
+
[TRACE_LEVEL, "trace"],
|
|
20
|
+
[DEBUG_LEVEL, "debug"],
|
|
21
|
+
[INFO_LEVEL, "info"],
|
|
22
|
+
[WARNING_LEVEL, "warn"],
|
|
23
|
+
[ERROR_LEVEL, "error"],
|
|
24
|
+
[CRITICAL_LEVEL, "fatal"]
|
|
25
|
+
]);
|
|
26
|
+
/** @type {ReadonlyMap<string, Severity>} */
|
|
27
|
+
const PINO_LEVEL_NAMES = new Map([
|
|
28
|
+
["trace", "info"],
|
|
29
|
+
["debug", "info"],
|
|
30
|
+
["warn", "warning"],
|
|
31
|
+
["warning", "warning"],
|
|
32
|
+
["error", "error"],
|
|
33
|
+
["fatal", "critical"],
|
|
34
|
+
["critical", "critical"]
|
|
35
|
+
]);
|
|
36
|
+
const PINO_HOST_FIELD = ["host", "name"].join("");
|
|
37
|
+
const PINO_RESERVED_FIELDS = new Set(["level", "time", "timestamp", "msg", "message", "err", "error", "pid", PINO_HOST_FIELD, "v"]);
|
|
38
|
+
const PINO_SENSITIVE_CONTEXT_FIELDS = new Set([
|
|
39
|
+
"apikey",
|
|
40
|
+
"authorization",
|
|
41
|
+
"baggage",
|
|
42
|
+
"body",
|
|
43
|
+
"cookie",
|
|
44
|
+
"credentials",
|
|
45
|
+
"cwd",
|
|
46
|
+
"directory",
|
|
47
|
+
"dirname",
|
|
48
|
+
"errorstack",
|
|
49
|
+
"file",
|
|
50
|
+
"filename",
|
|
51
|
+
"filepath",
|
|
52
|
+
"headers",
|
|
53
|
+
"href",
|
|
54
|
+
"key",
|
|
55
|
+
"password",
|
|
56
|
+
"payload",
|
|
57
|
+
"proxyauthorization",
|
|
58
|
+
"query",
|
|
59
|
+
"querystring",
|
|
60
|
+
"requestbody",
|
|
61
|
+
"requestheaders",
|
|
62
|
+
"requesturl",
|
|
63
|
+
"responsebody",
|
|
64
|
+
"responseheaders",
|
|
65
|
+
"responseurl",
|
|
66
|
+
"search",
|
|
67
|
+
"secret",
|
|
68
|
+
"setcookie",
|
|
69
|
+
"stack",
|
|
70
|
+
"token",
|
|
71
|
+
"traceparent",
|
|
72
|
+
"tracestate",
|
|
73
|
+
"uri",
|
|
74
|
+
"url"
|
|
75
|
+
]);
|
|
76
|
+
const PINO_SENSITIVE_FIELD_SUFFIXES = [
|
|
77
|
+
"accesskey",
|
|
78
|
+
"apikey",
|
|
79
|
+
"authorization",
|
|
80
|
+
"body",
|
|
81
|
+
"cookie",
|
|
82
|
+
"credentials",
|
|
83
|
+
"headers",
|
|
84
|
+
"password",
|
|
85
|
+
"payload",
|
|
86
|
+
"privatekey",
|
|
87
|
+
"query",
|
|
88
|
+
"secret",
|
|
89
|
+
"stack",
|
|
90
|
+
"token",
|
|
91
|
+
"url"
|
|
92
|
+
];
|
|
93
|
+
const FIELD_SEPARATOR_PATTERN = /[^a-z0-9]+/gu;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {unknown} level
|
|
97
|
+
* @returns {Severity}
|
|
98
|
+
*/
|
|
99
|
+
function logbrewLevelFromPinoLevel(level) {
|
|
100
|
+
if (typeof level === "number" && Number.isFinite(level)) {
|
|
101
|
+
if (level >= CRITICAL_LEVEL) {
|
|
102
|
+
return "critical";
|
|
103
|
+
}
|
|
104
|
+
if (level >= ERROR_LEVEL) {
|
|
105
|
+
return "error";
|
|
106
|
+
}
|
|
107
|
+
return level >= WARNING_LEVEL ? "warning" : "info";
|
|
108
|
+
}
|
|
109
|
+
return PINO_LEVEL_NAMES.get(String(level).toLowerCase()) ?? "info";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} level
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function pinoLevelLabel(level) {
|
|
117
|
+
if (typeof level === "string" && level.trim() !== "") {
|
|
118
|
+
return level;
|
|
119
|
+
}
|
|
120
|
+
if (typeof level !== "number" || !Number.isFinite(level)) {
|
|
121
|
+
return "info";
|
|
122
|
+
}
|
|
123
|
+
return PINO_LEVEL_LABELS.get(level) ?? String(level);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {Readonly<Record<string, unknown>>} record
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function pinoMessage(record) {
|
|
131
|
+
const { msg, message } = record;
|
|
132
|
+
const error = record["err"] ?? record["error"];
|
|
133
|
+
const errorMessage = isObjectValue(error) ? error["message"] : undefined;
|
|
134
|
+
return stringOrUndefined(msg) ?? stringOrUndefined(message) ?? stringOrUndefined(errorMessage) ?? "pino event";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} key
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function isSensitivePinoContextField(key) {
|
|
142
|
+
const normalized = key.toLowerCase().replaceAll(FIELD_SEPARATOR_PATTERN, "");
|
|
143
|
+
return PINO_SENSITIVE_CONTEXT_FIELDS.has(normalized)
|
|
144
|
+
|| PINO_SENSITIVE_FIELD_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {Readonly<Record<string, unknown>>} record
|
|
149
|
+
* @returns {Record<string, unknown>}
|
|
150
|
+
*/
|
|
151
|
+
function pinoContextMetadata(record) {
|
|
152
|
+
/** @type {Record<string, unknown>} */
|
|
153
|
+
const metadata = {};
|
|
154
|
+
for (const [key, value] of Object.entries(record)) {
|
|
155
|
+
if (!PINO_RESERVED_FIELDS.has(key) && !isSensitivePinoContextField(key) && isMetadataValue(value)) {
|
|
156
|
+
metadata[`context.${key}`] = value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return metadata;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {unknown} error
|
|
164
|
+
* @param {boolean} includeErrorStack
|
|
165
|
+
* @returns {Record<string, unknown>}
|
|
166
|
+
*/
|
|
167
|
+
function pinoErrorMetadata(error, includeErrorStack) {
|
|
168
|
+
if (error instanceof Error) {
|
|
169
|
+
return {
|
|
170
|
+
errorName: isTruthy(error.name) ? error.name : "Error",
|
|
171
|
+
...(isTruthy(error.message) ? { errorMessage: error.message } : {}),
|
|
172
|
+
...(includeErrorStack && isTruthy(error.stack) ? { errorStack: error.stack } : {})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (isObjectValue(error)) {
|
|
176
|
+
const name = stringOrUndefined(error["type"] ?? error["name"]);
|
|
177
|
+
const message = stringOrUndefined(error["message"]);
|
|
178
|
+
const stack = stringOrUndefined(error["stack"]);
|
|
179
|
+
return {
|
|
180
|
+
...(name === undefined ? {} : { errorName: name }),
|
|
181
|
+
...(message === undefined ? {} : { errorMessage: message }),
|
|
182
|
+
...(includeErrorStack && stack !== undefined ? { errorStack: stack } : {})
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return typeof error === "string" && error.trim() !== "" ? { errorMessage: error } : {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {unknown} record
|
|
190
|
+
* @param {PinoRecordOptions} [options]
|
|
191
|
+
* @returns {PinoLogAttributes}
|
|
192
|
+
*/
|
|
193
|
+
function logAttributesFromPinoRecord(record, options = {}) {
|
|
194
|
+
if (!isRecord(record)) {
|
|
195
|
+
throw new SdkError("validation_error", "Pino record must be an object");
|
|
196
|
+
}
|
|
197
|
+
const level = logbrewLevelFromPinoLevel(record["level"]);
|
|
198
|
+
const metadata = {
|
|
199
|
+
...compactMetadata(options.metadata),
|
|
200
|
+
pinoLevel: pinoLevelLabel(record["level"]),
|
|
201
|
+
...pinoContextMetadata(record),
|
|
202
|
+
...traceMetadataFromLogContext(options.trace)
|
|
203
|
+
};
|
|
204
|
+
const pinoLevelNumber = record["level"];
|
|
205
|
+
const errorMetadata = pinoErrorMetadata(record["err"] ?? record["error"], options.includeErrorStack === true);
|
|
206
|
+
return {
|
|
207
|
+
message: pinoMessage(record),
|
|
208
|
+
|
|
209
|
+
level,
|
|
210
|
+
...(isTruthy(options.logger) ? { logger: options.logger } : {}),
|
|
211
|
+
|
|
212
|
+
metadata: {
|
|
213
|
+
...metadata,
|
|
214
|
+
...(typeof pinoLevelNumber === "number" && Number.isFinite(pinoLevelNumber) ? { pinoLevelNumber } : {}),
|
|
215
|
+
...errorMetadata
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {unknown} record
|
|
222
|
+
* @param {() => unknown} fallbackTimestamp
|
|
223
|
+
* @returns {unknown}
|
|
224
|
+
*/
|
|
225
|
+
function timestampFromPinoRecord(record, fallbackTimestamp) {
|
|
226
|
+
const value = isObjectValue(record) ? record["time"] ?? record["timestamp"] : undefined;
|
|
227
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
228
|
+
return new Date(value).toISOString();
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
231
|
+
const parsed = new Date(value);
|
|
232
|
+
return Number.isNaN(parsed.valueOf()) ? value : parsed.toISOString();
|
|
233
|
+
}
|
|
234
|
+
return fallbackTimestamp();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { logAttributesFromPinoRecord, timestampFromPinoRecord };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const { ACTION_STATUSES } = require("./event-validation.cjs");
|
|
2
|
+
const { compactMetadata } = require("./log-context.cjs");
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { hasUnicodeControlCharacter, isRecord, requireAllowedValue, requireNonEmpty, stringOrUndefined } = require("./validation.cjs");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import { ActionAttributes, MetadataValue, ProductAnalyticsKind } from "./index.cjs"
|
|
8
|
+
* @typedef {ActionAttributes["status"]} ActionStatus
|
|
9
|
+
* @typedef {Readonly<{ metadata?: unknown }>} TimelineOptions
|
|
10
|
+
* @typedef {{ name: string, status: ActionStatus, metadata: Record<string, MetadataValue> }} TimelineAttributes
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* funnel?: unknown,
|
|
13
|
+
* metadata?: unknown,
|
|
14
|
+
* name: string,
|
|
15
|
+
* routeTemplate?: unknown,
|
|
16
|
+
* screen?: unknown,
|
|
17
|
+
* sessionId?: unknown,
|
|
18
|
+
* status: ActionStatus,
|
|
19
|
+
* step?: unknown,
|
|
20
|
+
* traceId?: unknown
|
|
21
|
+
* }} ProductActionDetails
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {1} */
|
|
25
|
+
const PRODUCT_ANALYTICS_SCHEMA_VERSION = 1;
|
|
26
|
+
/** @type {readonly ProductAnalyticsKind[]} */
|
|
27
|
+
const PRODUCT_ANALYTICS_KINDS = Object.freeze(["page_view", "screen_view", "interaction"]);
|
|
28
|
+
const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
|
|
29
|
+
const ROUTE_BASE_URL = "https://logbrew.example";
|
|
30
|
+
const ROUTE_LOCATION_PATTERN = /[?#]/u;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} trimmed
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function routePathname(trimmed) {
|
|
37
|
+
try {
|
|
38
|
+
const { pathname } = new URL(trimmed, ROUTE_BASE_URL);
|
|
39
|
+
return pathname === "" ? "/" : pathname;
|
|
40
|
+
} catch {
|
|
41
|
+
const [path] = trimmed.split(ROUTE_LOCATION_PATTERN);
|
|
42
|
+
return path === undefined || path === "" ? "/" : path;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {unknown} routeTemplate
|
|
48
|
+
* @returns {string | undefined}
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeRouteTemplate(routeTemplate) {
|
|
51
|
+
if (routeTemplate === undefined) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (typeof routeTemplate !== "string") {
|
|
55
|
+
throw new SdkError("validation_error", "routeTemplate must be a string");
|
|
56
|
+
}
|
|
57
|
+
const trimmed = routeTemplate.trim();
|
|
58
|
+
return trimmed === "" ? "" : routePathname(trimmed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} surface
|
|
63
|
+
* @returns {string | undefined}
|
|
64
|
+
*/
|
|
65
|
+
function boundedProductAnalyticsSurface(surface) {
|
|
66
|
+
if (typeof surface !== "string") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const normalized = surface.trim();
|
|
70
|
+
if (
|
|
71
|
+
normalized === ""
|
|
72
|
+
|| [...normalized].length > MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH
|
|
73
|
+
|| hasUnicodeControlCharacter(normalized)
|
|
74
|
+
) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {ProductAnalyticsKind} kind
|
|
82
|
+
* @param {unknown} surface
|
|
83
|
+
* @returns {Record<string, unknown>}
|
|
84
|
+
*/
|
|
85
|
+
function productAnalyticsMetadata(kind, surface) {
|
|
86
|
+
return {
|
|
87
|
+
analyticsSchemaVersion: PRODUCT_ANALYTICS_SCHEMA_VERSION,
|
|
88
|
+
|
|
89
|
+
analyticsKind: kind,
|
|
90
|
+
analyticsSurface: boundedProductAnalyticsSurface(surface)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {unknown} action
|
|
96
|
+
* @returns {ProductActionDetails}
|
|
97
|
+
*/
|
|
98
|
+
function productActionDetails(action) {
|
|
99
|
+
if (typeof action === "string") {
|
|
100
|
+
return { name: action, status: "success" };
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(action)) {
|
|
103
|
+
throw new SdkError("validation_error", "product action must be a string or object");
|
|
104
|
+
}
|
|
105
|
+
const name = requireNonEmpty("product action name", action["name"]);
|
|
106
|
+
const status = requireAllowedValue(
|
|
107
|
+
"product action status",
|
|
108
|
+
action["status"] === undefined ? "success" : action["status"],
|
|
109
|
+
ACTION_STATUSES
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
funnel: action["funnel"],
|
|
113
|
+
metadata: action["metadata"],
|
|
114
|
+
name,
|
|
115
|
+
routeTemplate: action["routeTemplate"],
|
|
116
|
+
screen: action["screen"],
|
|
117
|
+
sessionId: action["sessionId"],
|
|
118
|
+
status,
|
|
119
|
+
step: action["step"],
|
|
120
|
+
traceId: action["traceId"]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {unknown} action
|
|
126
|
+
* @param {TimelineOptions} [options]
|
|
127
|
+
* @returns {TimelineAttributes}
|
|
128
|
+
*/
|
|
129
|
+
function createProductActionAttributes(action, options = {}) {
|
|
130
|
+
const details = productActionDetails(action);
|
|
131
|
+
const routeTemplate = sanitizeRouteTemplate(details.routeTemplate);
|
|
132
|
+
const surface = routeTemplate === undefined || routeTemplate === "" ? details.screen : routeTemplate;
|
|
133
|
+
return {
|
|
134
|
+
name: details.name,
|
|
135
|
+
|
|
136
|
+
status: details.status,
|
|
137
|
+
|
|
138
|
+
metadata: compactMetadata({
|
|
139
|
+
source: "product.action",
|
|
140
|
+
...compactMetadata(options.metadata),
|
|
141
|
+
...compactMetadata(details.metadata),
|
|
142
|
+
routeTemplate,
|
|
143
|
+
|
|
144
|
+
sessionId: stringOrUndefined(details.sessionId),
|
|
145
|
+
|
|
146
|
+
traceId: stringOrUndefined(details.traceId),
|
|
147
|
+
|
|
148
|
+
screen: stringOrUndefined(details.screen),
|
|
149
|
+
|
|
150
|
+
funnel: stringOrUndefined(details.funnel),
|
|
151
|
+
|
|
152
|
+
step: stringOrUndefined(details.step),
|
|
153
|
+
...productAnalyticsMetadata("interaction", surface)
|
|
154
|
+
})
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
PRODUCT_ANALYTICS_KINDS,
|
|
160
|
+
PRODUCT_ANALYTICS_SCHEMA_VERSION,
|
|
161
|
+
createProductActionAttributes,
|
|
162
|
+
sanitizeRouteTemplate
|
|
163
|
+
};
|