New:Socket for Asana Is Now Available.Learn more
Get Started

@posthog/core

Package Overview
Dependencies
Maintainers
21
Versions
183
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@posthog/core - npm Package Compare versions

Comparing version
1.48.6
to
1.48.7
+1
-1
dist/logs/index.js

@@ -77,3 +77,3 @@ "use strict";

if (!this._checkRateLimit()) return;
const record = (0, external_logs_utils_js_namespaceObject.buildOtlpLogRecord)(filtered, this._getContext());
const record = (0, external_logs_utils_js_namespaceObject.buildOtlpLogRecord)(filtered, this._getContext(), this._logger);
const entry = {

@@ -80,0 +80,0 @@ record

@@ -49,3 +49,3 @@ import { buildOtlpLogRecord, buildOtlpLogsPayload, buildResourceAttributes } from "./logs-utils.mjs";

if (!this._checkRateLimit()) return;
const record = buildOtlpLogRecord(filtered, this._getContext());
const record = buildOtlpLogRecord(filtered, this._getContext(), this._logger);
const entry = {

@@ -52,0 +52,0 @@ record

import type { CaptureLogOptions, LogAttributeValue, LogSeverityLevel, OtlpAnyValue, OtlpKeyValue, OtlpLogRecord, OtlpLogsPayload, OtlpSeverityText } from '@posthog/types';
import type { Logger } from '../types';
import type { LogSdkContext, ResolvedPostHogLogsConfig } from './types';
export declare function getOtlpSeverityText(level: LogSeverityLevel): OtlpSeverityText;
export declare function getOtlpSeverityNumber(level: LogSeverityLevel): number;
export declare function toOtlpAnyValue(value: LogAttributeValue): OtlpAnyValue;
export declare function toOtlpKeyValueList(attrs: Record<string, LogAttributeValue>): OtlpKeyValue[];
export declare function toOtlpAnyValue(value: LogAttributeValue, logger?: Logger): OtlpAnyValue;
export declare function toOtlpKeyValueList(attrs: Record<string, LogAttributeValue>, logger?: Logger): OtlpKeyValue[];
export declare function buildOtlpLogRecord(options: CaptureLogOptions, sdkContext: LogSdkContext, logger?: Logger): OtlpLogRecord;
/**
* Builds a single OTLP log record.
*
* Auto-attribute population is shape-driven: any field present on `sdkContext`
* is emitted as the corresponding attribute. Each SDK populates only the
* fields that apply to it (browser fills `currentUrl`; mobile fills
* `screenName` / `appState`), so a missing field never adds a stray attribute.
*
* User-provided `options.attributes` always wins on conflicts.
*/
export declare function buildOtlpLogRecord(options: CaptureLogOptions, sdkContext: LogSdkContext): OtlpLogRecord;
/**
* OTLP resource attributes for every batch, shared by the core flush path and

@@ -20,0 +11,0 @@ * SDK-specific paths that bypass it (e.g. the browser's synchronous sendBeacon

@@ -1,1 +0,1 @@

{"version":3,"file":"logs-utils.d.ts","sourceRoot":"","sources":["../../src/logs/logs-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,eAAe,EAEf,gBAAgB,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAkBvE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAE7E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAiCrE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,YAAY,EAAE,CAU3F;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,GAAG,aAAa,CA4DvG;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CASnC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,aAAa,EAAE,EAC3B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,eAAe,CAcjB"}
{"version":3,"file":"logs-utils.d.ts","sourceRoot":"","sources":["../../src/logs/logs-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,eAAe,EAEf,gBAAgB,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AA4BvE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAE7E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAqBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAQtF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CAM5G;AAmMD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,aAAa,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,aAAa,CA+Ef;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CASnC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,aAAa,EAAE,EAC3B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,eAAe,CAcjB"}

@@ -36,2 +36,3 @@ "use strict";

const index_js_namespaceObject = require("../utils/index.js");
const json_utils_js_namespaceObject = require("../utils/json-utils.js");
const OTLP_SEVERITY_MAP = {

@@ -70,3 +71,31 @@ trace: {

}
function toOtlpAnyValue(value) {
const INT64_RANGE_LIMIT = 9223372036854775808;
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
function newState() {
return {
ancestors: new WeakSet(),
remainingNodes: json_utils_js_namespaceObject.MAX_JSON_SAFE_VALUE_NODES
};
}
function toOtlpAnyValue(value, logger) {
try {
return encodeAnyValue(value, logger, newState(), 0);
} catch {
return {
stringValue: json_utils_js_namespaceObject.UNSERIALIZABLE_VALUE
};
}
}
function toOtlpKeyValueList(attrs, logger) {
try {
return encodeKeyValueList(attrs, logger, newState(), 0);
} catch {
return [];
}
}
function encodeAnyValue(value, logger, state, depth) {
if (state.remainingNodes <= 0) return {
stringValue: json_utils_js_namespaceObject.TRUNCATED_VALUE
};
state.remainingNodes--;
if ((0, index_js_namespaceObject.isBoolean)(value)) return {

@@ -79,5 +108,20 @@ boolValue: value

};
if (Number.isInteger(value)) return {
intValue: value
};
if (Number.isInteger(value)) {
if (Number.isSafeInteger(value)) return {
intValue: String(value)
};
if ('undefined' == typeof BigInt) return {
stringValue: String(value)
};
const decimal = BigInt(value).toString();
if (value >= INT64_RANGE_LIMIT || value < -INT64_RANGE_LIMIT) {
logger?.debug(`Attribute ${decimal} is outside the int64 range; encoding it as a string`);
return {
stringValue: decimal
};
}
return {
intValue: decimal
};
}
return {

@@ -88,34 +132,102 @@ doubleValue: value

if ('string' == typeof value) return {
stringValue: value
stringValue: (0, json_utils_js_namespaceObject.sanitizeString)(value)
};
if ((0, index_js_namespaceObject.isArray)(value)) return {
arrayValue: {
values: value.map((v)=>toOtlpAnyValue(v))
}
if ('function' == typeof value) return {
stringValue: json_utils_js_namespaceObject.FUNCTION_VALUE
};
try {
return {
stringValue: JSON.stringify(value)
if ('symbol' == typeof value) return {
stringValue: String(value)
};
if ('object' == typeof value && null !== value) {
if (state.ancestors.has(value)) return {
stringValue: json_utils_js_namespaceObject.CIRCULAR_VALUE
};
} catch {
return {
stringValue: String(value)
if (depth >= json_utils_js_namespaceObject.MAX_JSON_SAFE_VALUE_DEPTH) return {
stringValue: json_utils_js_namespaceObject.TRUNCATED_VALUE
};
if (value instanceof Date) {
const time = value.getTime();
const iso = Number.isFinite(time) ? value.toISOString() : String(value);
return {
stringValue: 'string' == typeof iso ? (0, json_utils_js_namespaceObject.sanitizeString)(iso) : String(iso)
};
}
state.ancestors.add(value);
try {
try {
const toJSON = value.toJSON;
if ('function' == typeof toJSON) return encodeAnyValue(toJSON.call(value), logger, state, depth + 1);
} catch {}
if ((0, index_js_namespaceObject.isArray)(value)) return {
arrayValue: {
values: encodeArrayValues(value, logger, state, depth + 1)
}
};
return {
kvlistValue: {
values: encodeKeyValueList(value, logger, state, depth + 1)
}
};
} finally{
state.ancestors.delete(value);
}
}
return {
stringValue: (0, json_utils_js_namespaceObject.sanitizeString)(String(value))
};
}
function toOtlpKeyValueList(attrs) {
function encodeArrayValues(values, logger, state, depth) {
const result = [];
for(const key in attrs){
const value = attrs[key];
if (!((0, index_js_namespaceObject.isNull)(value) || (0, index_js_namespaceObject.isUndefined)(value))) result.push({
key,
value: toOtlpAnyValue(value)
const itemCount = Math.min(values.length, json_utils_js_namespaceObject.MAX_JSON_SAFE_VALUE_ITEMS);
let index = 0;
for(; index < itemCount && state.remainingNodes > 0; index++)try {
const element = index in values ? values[index] : void 0;
if ((0, index_js_namespaceObject.isNullish)(element)) continue;
result.push(encodeAnyValue(element, logger, state, depth));
} catch {
result.push({
stringValue: json_utils_js_namespaceObject.UNSERIALIZABLE_VALUE
});
}
if (values.length > index) result.push({
stringValue: json_utils_js_namespaceObject.TRUNCATED_VALUE
});
return result;
}
function encodeKeyValueList(attrs, logger, state, depth) {
const result = [];
for(const key in attrs)if (propertyIsEnumerable.call(attrs, key)) {
if (result.length >= json_utils_js_namespaceObject.MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
logger?.debug('Attributes truncated: the value exceeds the OTLP encoder budget');
break;
}
try {
const value = attrs[key];
if ((0, index_js_namespaceObject.isNull)(value) || (0, index_js_namespaceObject.isUndefined)(value)) continue;
result.push({
key: (0, json_utils_js_namespaceObject.sanitizeString)(key),
value: encodeAnyValue(value, logger, state, depth)
});
} catch {
result.push({
key: (0, json_utils_js_namespaceObject.sanitizeString)(key),
value: {
stringValue: json_utils_js_namespaceObject.UNSERIALIZABLE_VALUE
}
});
}
}
return result;
}
function timestampToUnixNano() {
return String(Date.now()) + '000000';
}
function buildOtlpLogRecord(options, sdkContext) {
function encodeBody(body) {
try {
return (0, json_utils_js_namespaceObject.sanitizeString)(String(body));
} catch {
return json_utils_js_namespaceObject.UNSERIALIZABLE_VALUE;
}
}
function buildOtlpLogRecord(options, sdkContext, logger) {
const level = options.level || 'info';

@@ -135,5 +247,27 @@ const { text: severityText, number: severityNumber } = OTLP_SEVERITY_MAP[level] || DEFAULT_OTLP_SEVERITY;

const mergedAttributes = {
...autoAttributes,
...options.attributes || {}
...autoAttributes
};
const userAttributes = options.attributes;
if (userAttributes) {
let keys = [];
try {
keys = Object.keys(userAttributes);
} catch {
keys = [];
}
for (const key of keys){
let value;
try {
value = userAttributes[key];
} catch {
value = json_utils_js_namespaceObject.UNSERIALIZABLE_VALUE;
}
Object.defineProperty(mergedAttributes, key, {
value,
enumerable: true,
writable: true,
configurable: true
});
}
}
const record = {

@@ -145,5 +279,5 @@ timeUnixNano: now,

body: {
stringValue: options.body
stringValue: encodeBody(options.body)
},
attributes: toOtlpKeyValueList(mergedAttributes)
attributes: toOtlpKeyValueList(mergedAttributes, logger)
};

@@ -150,0 +284,0 @@ if (options.trace_id) record.traceId = options.trace_id;

import { isArray, isBoolean, isNull, isNullish, isUndefined } from "../utils/index.mjs";
import { CIRCULAR_VALUE, FUNCTION_VALUE, MAX_JSON_SAFE_VALUE_DEPTH, MAX_JSON_SAFE_VALUE_ITEMS, MAX_JSON_SAFE_VALUE_NODES, TRUNCATED_VALUE, UNSERIALIZABLE_VALUE, sanitizeString } from "../utils/json-utils.mjs";
const OTLP_SEVERITY_MAP = {

@@ -35,3 +36,31 @@ trace: {

}
function toOtlpAnyValue(value) {
const INT64_RANGE_LIMIT = 9223372036854775808;
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
function newState() {
return {
ancestors: new WeakSet(),
remainingNodes: MAX_JSON_SAFE_VALUE_NODES
};
}
function toOtlpAnyValue(value, logger) {
try {
return encodeAnyValue(value, logger, newState(), 0);
} catch {
return {
stringValue: UNSERIALIZABLE_VALUE
};
}
}
function toOtlpKeyValueList(attrs, logger) {
try {
return encodeKeyValueList(attrs, logger, newState(), 0);
} catch {
return [];
}
}
function encodeAnyValue(value, logger, state, depth) {
if (state.remainingNodes <= 0) return {
stringValue: TRUNCATED_VALUE
};
state.remainingNodes--;
if (isBoolean(value)) return {

@@ -44,5 +73,20 @@ boolValue: value

};
if (Number.isInteger(value)) return {
intValue: value
};
if (Number.isInteger(value)) {
if (Number.isSafeInteger(value)) return {
intValue: String(value)
};
if ('undefined' == typeof BigInt) return {
stringValue: String(value)
};
const decimal = BigInt(value).toString();
if (value >= INT64_RANGE_LIMIT || value < -INT64_RANGE_LIMIT) {
logger?.debug(`Attribute ${decimal} is outside the int64 range; encoding it as a string`);
return {
stringValue: decimal
};
}
return {
intValue: decimal
};
}
return {

@@ -53,34 +97,102 @@ doubleValue: value

if ('string' == typeof value) return {
stringValue: value
stringValue: sanitizeString(value)
};
if (isArray(value)) return {
arrayValue: {
values: value.map((v)=>toOtlpAnyValue(v))
}
if ('function' == typeof value) return {
stringValue: FUNCTION_VALUE
};
try {
return {
stringValue: JSON.stringify(value)
if ('symbol' == typeof value) return {
stringValue: String(value)
};
if ('object' == typeof value && null !== value) {
if (state.ancestors.has(value)) return {
stringValue: CIRCULAR_VALUE
};
} catch {
return {
stringValue: String(value)
if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) return {
stringValue: TRUNCATED_VALUE
};
if (value instanceof Date) {
const time = value.getTime();
const iso = Number.isFinite(time) ? value.toISOString() : String(value);
return {
stringValue: 'string' == typeof iso ? sanitizeString(iso) : String(iso)
};
}
state.ancestors.add(value);
try {
try {
const toJSON = value.toJSON;
if ('function' == typeof toJSON) return encodeAnyValue(toJSON.call(value), logger, state, depth + 1);
} catch {}
if (isArray(value)) return {
arrayValue: {
values: encodeArrayValues(value, logger, state, depth + 1)
}
};
return {
kvlistValue: {
values: encodeKeyValueList(value, logger, state, depth + 1)
}
};
} finally{
state.ancestors.delete(value);
}
}
return {
stringValue: sanitizeString(String(value))
};
}
function toOtlpKeyValueList(attrs) {
function encodeArrayValues(values, logger, state, depth) {
const result = [];
for(const key in attrs){
const value = attrs[key];
if (!(isNull(value) || isUndefined(value))) result.push({
key,
value: toOtlpAnyValue(value)
const itemCount = Math.min(values.length, MAX_JSON_SAFE_VALUE_ITEMS);
let index = 0;
for(; index < itemCount && state.remainingNodes > 0; index++)try {
const element = index in values ? values[index] : void 0;
if (isNullish(element)) continue;
result.push(encodeAnyValue(element, logger, state, depth));
} catch {
result.push({
stringValue: UNSERIALIZABLE_VALUE
});
}
if (values.length > index) result.push({
stringValue: TRUNCATED_VALUE
});
return result;
}
function encodeKeyValueList(attrs, logger, state, depth) {
const result = [];
for(const key in attrs)if (propertyIsEnumerable.call(attrs, key)) {
if (result.length >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
logger?.debug('Attributes truncated: the value exceeds the OTLP encoder budget');
break;
}
try {
const value = attrs[key];
if (isNull(value) || isUndefined(value)) continue;
result.push({
key: sanitizeString(key),
value: encodeAnyValue(value, logger, state, depth)
});
} catch {
result.push({
key: sanitizeString(key),
value: {
stringValue: UNSERIALIZABLE_VALUE
}
});
}
}
return result;
}
function timestampToUnixNano() {
return String(Date.now()) + '000000';
}
function buildOtlpLogRecord(options, sdkContext) {
function encodeBody(body) {
try {
return sanitizeString(String(body));
} catch {
return UNSERIALIZABLE_VALUE;
}
}
function buildOtlpLogRecord(options, sdkContext, logger) {
const level = options.level || 'info';

@@ -100,5 +212,27 @@ const { text: severityText, number: severityNumber } = OTLP_SEVERITY_MAP[level] || DEFAULT_OTLP_SEVERITY;

const mergedAttributes = {
...autoAttributes,
...options.attributes || {}
...autoAttributes
};
const userAttributes = options.attributes;
if (userAttributes) {
let keys = [];
try {
keys = Object.keys(userAttributes);
} catch {
keys = [];
}
for (const key of keys){
let value;
try {
value = userAttributes[key];
} catch {
value = UNSERIALIZABLE_VALUE;
}
Object.defineProperty(mergedAttributes, key, {
value,
enumerable: true,
writable: true,
configurable: true
});
}
}
const record = {

@@ -110,5 +244,5 @@ timeUnixNano: now,

body: {
stringValue: options.body
stringValue: encodeBody(options.body)
},
attributes: toOtlpKeyValueList(mergedAttributes)
attributes: toOtlpKeyValueList(mergedAttributes, logger)
};

@@ -115,0 +249,0 @@ if (options.trace_id) record.traceId = options.trace_id;

@@ -271,3 +271,3 @@ "use strict";

}
const attributes = (0, logs_utils_js_namespaceObject.toOtlpKeyValueList)(state.attributes ?? {});
const attributes = (0, logs_utils_js_namespaceObject.toOtlpKeyValueList)(state.attributes ?? {}, this._logger);
const startNano = (0, external_metrics_utils_js_namespaceObject.msToUnixNano)(state.windowStartMs);

@@ -274,0 +274,0 @@ if ('count' === state.type) {

@@ -239,3 +239,3 @@ import { isArray, safeSetTimeout } from "../utils/index.mjs";

}
const attributes = toOtlpKeyValueList(state.attributes ?? {});
const attributes = toOtlpKeyValueList(state.attributes ?? {}, this._logger);
const startNano = msToUnixNano(state.windowStartMs);

@@ -242,0 +242,0 @@ if ('count' === state.type) {

@@ -5,3 +5,3 @@ import { FetchLike } from '../types';

export * from './bucketed-rate-limiter';
export * from './json-utils';
export { toJsonSafeValue } from './json-utils';
export * from './number-utils';

@@ -8,0 +8,0 @@ export * from './string-utils';

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAElC,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,QAAoE,CAAA;AAE3F,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9D;AASD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,CAMxG;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;CACtC;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqB5F;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAOnE;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,IAAI,GACrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAoBnB;AAGD,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,GAAG,IAAI,OAAO,CAAC,GAAG,CAEtD,CAAA;AAED,wBAAgB,QAAQ,IAAI,SAAS,GAAG,SAAS,CAEhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC1B,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAC1C,OAAO,CAAC,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,CAAC,CAStF"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AAGvC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAElC,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,QAAoE,CAAA;AAE3F,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9D;AASD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,CAMxG;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;CACtC;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqB5F;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAOnE;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,IAAI,GACrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAoBnB;AAGD,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,GAAG,IAAI,OAAO,CAAC,GAAG,CAEtD,CAAA;AAED,wBAAgB,QAAQ,IAAI,SAAS,GAAG,SAAS,CAEhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC1B,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAC1C,OAAO,CAAC,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,CAAC,CAStF"}

@@ -92,3 +92,4 @@ "use strict";

safeSetTimeout: ()=>safeSetTimeout,
stripUrlHash: ()=>stripUrlHash
stripUrlHash: ()=>stripUrlHash,
toJsonSafeValue: ()=>_json_utils__WEBPACK_IMPORTED_MODULE_3__.toJsonSafeValue
});

@@ -100,2 +101,3 @@ var _bot_detection__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./bot-detection");

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -124,2 +126,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -148,2 +151,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -168,24 +172,2 @@ "safeSetTimeout",

var _json_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("./json-utils");
var __WEBPACK_REEXPORT_OBJECT__ = {};
for(var __WEBPACK_IMPORT_KEY__ in _json_utils__WEBPACK_IMPORTED_MODULE_3__)if ([
"removeTrailingSlash",
"isValidUUID",
"currentISOTime",
"safeSetTimeout",
"UUID_REGEX",
"assert",
"allSettled",
"retriable",
"default",
"stripUrlHash",
"STRING_FORMAT",
"currentTimestamp",
"raceWithTimeout",
"getFetch",
"getEventUuid",
"isPromise"
].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
return _json_utils__WEBPACK_IMPORTED_MODULE_3__[key];
}).bind(0, __WEBPACK_IMPORT_KEY__);
__webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
var _number_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./number-utils");

@@ -196,2 +178,3 @@ var __WEBPACK_REEXPORT_OBJECT__ = {};

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -220,2 +203,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -244,2 +228,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -268,2 +253,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -292,2 +278,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -316,2 +303,3 @@ "safeSetTimeout",

"isValidUUID",
"toJsonSafeValue",
"currentISOTime",

@@ -431,2 +419,3 @@ "safeSetTimeout",

exports.stripUrlHash = __webpack_exports__.stripUrlHash;
exports.toJsonSafeValue = __webpack_exports__.toJsonSafeValue;
for(var __webpack_i__ in __webpack_exports__)if (-1 === [

@@ -447,3 +436,4 @@ "STRING_FORMAT",

"safeSetTimeout",
"stripUrlHash"
"stripUrlHash",
"toJsonSafeValue"
].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];

@@ -450,0 +440,0 @@ Object.defineProperty(exports, '__esModule', {

@@ -0,5 +1,5 @@

import { toJsonSafeValue } from "./json-utils.mjs";
export * from "./bot-detection.mjs";
export * from "./browser-utils.mjs";
export * from "./bucketed-rate-limiter.mjs";
export * from "./json-utils.mjs";
export * from "./number-utils.mjs";

@@ -91,2 +91,2 @@ export * from "./string-utils.mjs";

}
export { STRING_FORMAT, UUID_REGEX, allSettled, assert, currentISOTime, currentTimestamp, getEventUuid, getFetch, isPromise, isValidUUID, raceWithTimeout, removeTrailingSlash, retriable, safeSetTimeout, stripUrlHash };
export { STRING_FORMAT, UUID_REGEX, allSettled, assert, currentISOTime, currentTimestamp, getEventUuid, getFetch, isPromise, isValidUUID, raceWithTimeout, removeTrailingSlash, retriable, safeSetTimeout, stripUrlHash, toJsonSafeValue };

@@ -0,2 +1,17 @@

/** Traversal budgets and markers, shared with the OTLP attribute encoder. Not
* public API — `utils/index.ts` re-exports `toJsonSafeValue` only. */
export declare const MAX_JSON_SAFE_VALUE_DEPTH = 20;
export declare const MAX_JSON_SAFE_VALUE_ITEMS = 1000;
export declare const MAX_JSON_SAFE_VALUE_NODES = 10000;
export declare const CIRCULAR_VALUE = "[Circular]";
export declare const TRUNCATED_VALUE = "[Truncated]";
export declare const UNSERIALIZABLE_VALUE = "[Unserializable]";
export declare const FUNCTION_VALUE = "[Function]";
/**
* Replaces unpaired surrogates with U+FFFD. A lone surrogate survives
* `JSON.stringify` as a `\uD800`-style escape, which strict JSON parsers
* reject — for OTLP that means the whole request is refused.
*/
export declare function sanitizeString(value: string): string;
/**
* Converts an arbitrary value, sanitizing `toJSON` results when available.

@@ -3,0 +18,0 @@ * The limits keep pathological values from causing unbounded recursion or traversal.

@@ -1,1 +0,1 @@

{"version":3,"file":"json-utils.d.ts","sourceRoot":"","sources":["../../src/utils/json-utils.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2GvD"}
{"version":3,"file":"json-utils.d.ts","sourceRoot":"","sources":["../../src/utils/json-utils.ts"],"names":[],"mappings":"AAAA;sEACsE;AACtE,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C,eAAO,MAAM,yBAAyB,OAAQ,CAAA;AAC9C,eAAO,MAAM,yBAAyB,QAAS,CAAA;AAC/C,eAAO,MAAM,cAAc,eAAe,CAAA;AAC1C,eAAO,MAAM,eAAe,gBAAgB,CAAA;AAC5C,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AACtD,eAAO,MAAM,cAAc,eAAe,CAAA;AAW1C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBpD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2GvD"}

@@ -27,2 +27,10 @@ "use strict";

__webpack_require__.d(__webpack_exports__, {
CIRCULAR_VALUE: ()=>CIRCULAR_VALUE,
FUNCTION_VALUE: ()=>FUNCTION_VALUE,
MAX_JSON_SAFE_VALUE_DEPTH: ()=>MAX_JSON_SAFE_VALUE_DEPTH,
MAX_JSON_SAFE_VALUE_ITEMS: ()=>MAX_JSON_SAFE_VALUE_ITEMS,
MAX_JSON_SAFE_VALUE_NODES: ()=>MAX_JSON_SAFE_VALUE_NODES,
TRUNCATED_VALUE: ()=>TRUNCATED_VALUE,
UNSERIALIZABLE_VALUE: ()=>UNSERIALIZABLE_VALUE,
sanitizeString: ()=>sanitizeString,
toJsonSafeValue: ()=>toJsonSafeValue

@@ -123,4 +131,20 @@ });

}
exports.CIRCULAR_VALUE = __webpack_exports__.CIRCULAR_VALUE;
exports.FUNCTION_VALUE = __webpack_exports__.FUNCTION_VALUE;
exports.MAX_JSON_SAFE_VALUE_DEPTH = __webpack_exports__.MAX_JSON_SAFE_VALUE_DEPTH;
exports.MAX_JSON_SAFE_VALUE_ITEMS = __webpack_exports__.MAX_JSON_SAFE_VALUE_ITEMS;
exports.MAX_JSON_SAFE_VALUE_NODES = __webpack_exports__.MAX_JSON_SAFE_VALUE_NODES;
exports.TRUNCATED_VALUE = __webpack_exports__.TRUNCATED_VALUE;
exports.UNSERIALIZABLE_VALUE = __webpack_exports__.UNSERIALIZABLE_VALUE;
exports.sanitizeString = __webpack_exports__.sanitizeString;
exports.toJsonSafeValue = __webpack_exports__.toJsonSafeValue;
for(var __webpack_i__ in __webpack_exports__)if (-1 === [
"CIRCULAR_VALUE",
"FUNCTION_VALUE",
"MAX_JSON_SAFE_VALUE_DEPTH",
"MAX_JSON_SAFE_VALUE_ITEMS",
"MAX_JSON_SAFE_VALUE_NODES",
"TRUNCATED_VALUE",
"UNSERIALIZABLE_VALUE",
"sanitizeString",
"toJsonSafeValue"

@@ -127,0 +151,0 @@ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];

@@ -94,2 +94,2 @@ const MAX_JSON_SAFE_VALUE_DEPTH = 20;

}
export { toJsonSafeValue };
export { CIRCULAR_VALUE, FUNCTION_VALUE, MAX_JSON_SAFE_VALUE_DEPTH, MAX_JSON_SAFE_VALUE_ITEMS, MAX_JSON_SAFE_VALUE_NODES, TRUNCATED_VALUE, UNSERIALIZABLE_VALUE, sanitizeString, toJsonSafeValue };
{
"name": "@posthog/core",
"version": "1.48.6",
"version": "1.48.7",
"bugs": {

@@ -73,3 +73,3 @@ "url": "https://github.com/PostHog/posthog-js/issues"

"dependencies": {
"@posthog/types": "^1.405.0"
"@posthog/types": "^1.405.1"
},

@@ -76,0 +76,0 @@ "devDependencies": {

@@ -122,3 +122,3 @@ import type { LogAttributeValue } from '@posthog/types'

// not corrupt recorded attributes).
const record = buildOtlpLogRecord(filtered, this._getContext())
const record = buildOtlpLogRecord(filtered, this._getContext(), this._logger)
const entry: BufferedLogEntry = { record }

@@ -125,0 +125,0 @@

@@ -1,2 +0,2 @@

import type { CaptureLogOptions, LogSeverityLevel } from '@posthog/types'
import type { CaptureLogOptions, LogAttributeValue, LogSeverityLevel } from '@posthog/types'
import type { LogSdkContext } from './types'

@@ -72,8 +72,34 @@ import {

it('converts integers', () => {
expect(toOtlpAnyValue(42)).toEqual({ intValue: 42 })
expect(toOtlpAnyValue(0)).toEqual({ intValue: 0 })
expect(toOtlpAnyValue(-7)).toEqual({ intValue: -7 })
it('converts integers to decimal strings', () => {
expect(toOtlpAnyValue(42)).toEqual({ intValue: '42' })
expect(toOtlpAnyValue(0)).toEqual({ intValue: '0' })
expect(toOtlpAnyValue(-7)).toEqual({ intValue: '-7' })
})
// Spec: outside int64 it is a stringValue, never an intValue.
it('converts integers outside int64 to stringValue', () => {
expect(toOtlpAnyValue(2 ** 63)).toEqual({ stringValue: '9223372036854775808' })
expect(toOtlpAnyValue(-(2 ** 64))).toEqual({ stringValue: '-18446744073709551616' })
expect(toOtlpAnyValue(1e21)).toEqual({ stringValue: '1000000000000000000000' })
})
it('logs a debug line when an integer falls outside int64', () => {
const logger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), critical: jest.fn() }
toOtlpAnyValue(2 ** 63, logger as any)
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('outside the int64 range'))
})
it('keeps int64 min as intValue', () => {
// In range, but `String` renders it 192 below int64 min, so the decimal
// has to come from BigInt.
expect(toOtlpAnyValue(-(2 ** 63))).toEqual({ intValue: '-9223372036854775808' })
})
it('keeps large in-range integers exact', () => {
expect(toOtlpAnyValue(Number.MAX_SAFE_INTEGER)).toEqual({ intValue: '9007199254740991' })
// The largest double below 2^63 — no double exists between the two.
expect(toOtlpAnyValue(9223372036854774784)).toEqual({ intValue: '9223372036854774784' })
expect(toOtlpAnyValue(2 ** 62)).toEqual({ intValue: '4611686018427387904' })
})
it('converts floats to doubleValue', () => {

@@ -112,3 +138,3 @@ expect(toOtlpAnyValue(3.14)).toEqual({ doubleValue: 3.14 })

arrayValue: {
values: [{ intValue: 1 }, { stringValue: 'x' }, { boolValue: true }],
values: [{ intValue: '1' }, { stringValue: 'x' }, { boolValue: true }],
},

@@ -118,9 +144,288 @@ })

it('JSON-stringifies plain objects', () => {
it('converts plain objects to kvlistValue', () => {
expect(toOtlpAnyValue({ a: 1, b: 'two' })).toEqual({
stringValue: '{"a":1,"b":"two"}',
kvlistValue: {
values: [
{ key: 'a', value: { intValue: '1' } },
{ key: 'b', value: { stringValue: 'two' } },
],
},
})
})
it('converts nested objects recursively', () => {
expect(toOtlpAnyValue({ outer: { inner: 1 } })).toEqual({
kvlistValue: {
values: [
{
key: 'outer',
value: { kvlistValue: { values: [{ key: 'inner', value: { intValue: '1' } }] } },
},
],
},
})
})
it('drops null and undefined keys inside objects', () => {
expect(toOtlpAnyValue({ kept: 1, gone: null, alsoGone: undefined })).toEqual({
kvlistValue: { values: [{ key: 'kept', value: { intValue: '1' } }] },
})
})
// Not in LogAttributeValue, but reachable at runtime from untyped callers.
it('encodes Dates as ISO strings', () => {
expect(toOtlpAnyValue(new Date('2026-08-20T10:00:00.000Z') as unknown as LogAttributeValue)).toEqual({
stringValue: '2026-08-20T10:00:00.000Z',
})
})
it('marks circular references instead of recursing', () => {
const cyclic: Record<string, unknown> = { name: 'root' }
cyclic.self = cyclic
expect(toOtlpAnyValue(cyclic)).toEqual({
kvlistValue: {
values: [
{ key: 'name', value: { stringValue: 'root' } },
{ key: 'self', value: { stringValue: '[Circular]' } },
],
},
})
})
// An escaping error would surface in the caller's application code.
it('does not throw on an object nested past the depth cap', () => {
let deep: Record<string, unknown> = { end: true }
for (let i = 0; i < 25000; i++) {
deep = { next: deep }
}
expect(() => toOtlpAnyValue(deep)).not.toThrow()
})
it('truncates at exactly 20 levels instead of recursing', () => {
let deep: Record<string, unknown> = { end: true }
for (let i = 0; i < 25; i++) {
deep = { next: deep }
}
const encoded = JSON.stringify(toOtlpAnyValue(deep))
expect(encoded).toContain('[Truncated]')
expect(encoded.split('"next"').length - 1).toBe(20)
})
it('marks a throwing getter without losing the rest of the object', () => {
const attrs = {
ok: 1,
get bad(): number {
throw new Error('getter blew up')
},
}
expect(() => toOtlpKeyValueList(attrs)).not.toThrow()
expect(toOtlpKeyValueList(attrs)).toEqual([
{ key: 'ok', value: { intValue: '1' } },
{ key: 'bad', value: { stringValue: '[Unserializable]' } },
])
})
// for...in walks the prototype chain once own keys are exhausted.
it('ignores inherited enumerable properties', () => {
const inherited: Record<string, unknown> = Object.create({ fromPrototype: 'leaked' })
inherited.own = 1
expect(toOtlpAnyValue(inherited)).toEqual({
kvlistValue: { values: [{ key: 'own', value: { intValue: '1' } }] },
})
})
// `String(fn)` would put the function's source text on the wire.
it('marks function and symbol values instead of stringifying them', () => {
expect(toOtlpAnyValue({ handler: () => 1, retries: 2 } as unknown as LogAttributeValue)).toEqual({
kvlistValue: {
values: [
{ key: 'handler', value: { stringValue: '[Function]' } },
{ key: 'retries', value: { intValue: '2' } },
],
},
})
expect(toOtlpAnyValue({ sym: Symbol('x') } as unknown as LogAttributeValue)).toEqual({
kvlistValue: { values: [{ key: 'sym', value: { stringValue: 'Symbol(x)' } }] },
})
})
// dayjs, Decimal, ORM documents.
it('honours toJSON', () => {
const wrapped = { toJSON: () => ({ amount: 5 }) }
expect(toOtlpAnyValue(wrapped as unknown as LogAttributeValue)).toEqual({
kvlistValue: { values: [{ key: 'amount', value: { intValue: '5' } }] },
})
})
it('falls back to the plain walk when toJSON throws', () => {
const wrapped = {
kept: 1,
toJSON: () => {
throw new Error('nope')
},
}
expect(toOtlpAnyValue(wrapped as unknown as LogAttributeValue)).toEqual({
kvlistValue: {
values: [
{ key: 'kept', value: { intValue: '1' } },
{ key: 'toJSON', value: { stringValue: '[Function]' } },
],
},
})
})
// A toJSON returning its own object is a cycle like any other.
it('marks a cycle that runs through toJSON', () => {
const cyclic: Record<string, unknown> = {}
cyclic.toJSON = () => ({ inner: cyclic })
expect(toOtlpAnyValue(cyclic)).toEqual({
kvlistValue: { values: [{ key: 'inner', value: { stringValue: '[Circular]' } }] },
})
})
// Both `null` and `{}` here are rejected for the whole request; iOS and
// Android drop them too.
it('drops holes and nullish elements from arrays', () => {
// eslint-disable-next-line no-sparse-arrays
expect(toOtlpAnyValue([1, , 3])).toEqual({
arrayValue: { values: [{ intValue: '1' }, { intValue: '3' }] },
})
expect(toOtlpAnyValue([1, null, undefined, 3])).toEqual({
arrayValue: { values: [{ intValue: '1' }, { intValue: '3' }] },
})
})
it('stops encoding array items once the node budget is spent', () => {
const row: Record<string, number> = {}
for (let i = 0; i < 20; i++) {
row[`k${i}`] = i
}
const wide = Array.from({ length: 1000 }, () => ({ ...row }))
const values = toOtlpAnyValue(wide).arrayValue!.values
expect(values[values.length - 1]).toEqual({ stringValue: '[Truncated]' })
// One marker, not one per unencodable item.
expect(values.filter((v) => v.stringValue === '[Truncated]')).toHaveLength(1)
})
it('caps a shared object graph instead of expanding it', () => {
let graph: Record<string, unknown> = { leaf: true }
for (let i = 0; i < 20; i++) {
graph = { a: graph, b: graph }
}
const encoded = JSON.stringify(toOtlpAnyValue(graph))
expect(encoded).toContain('[Truncated]')
expect(encoded.length).toBeLessThan(1_000_000)
})
it('caps a very wide object without inventing an attribute key', () => {
const wide: Record<string, number> = {}
for (let i = 0; i < 5000; i++) {
wide[`k${i}`] = i
}
const logger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), critical: jest.fn() }
const values = toOtlpAnyValue(wide, logger as any).kvlistValue!.values
expect(values).toHaveLength(1000)
expect(values.every((v) => v.key.startsWith('k'))).toBe(true)
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('truncated'))
})
// Why the encoder does not delegate to toJsonSafeValue: that maps them to null.
it('keeps non-finite floats as strings inside nested objects', () => {
expect(toOtlpAnyValue({ nested: { ratio: NaN } })).toEqual({
kvlistValue: {
values: [
{
key: 'nested',
value: { kvlistValue: { values: [{ key: 'ratio', value: { stringValue: 'NaN' } }] } },
},
],
},
})
})
// A lone surrogate survives JSON.stringify as a \uD800 escape, which the
// server rejects for the whole request.
it('replaces unpaired surrogates in values and keys', () => {
expect(toOtlpAnyValue('ok\ud83d')).toEqual({ stringValue: 'ok\ufffd' })
expect(toOtlpAnyValue({ nested: 'ok\ud83d' })).toEqual({
kvlistValue: { values: [{ key: 'nested', value: { stringValue: 'ok\ufffd' } }] },
})
expect(toOtlpKeyValueList({ 'key\ud83d': 1 })).toEqual([{ key: 'key\ufffd', value: { intValue: '1' } }])
})
it('encodes empty containers with an explicit values array', () => {
expect(toOtlpAnyValue({})).toEqual({ kvlistValue: { values: [] } })
expect(toOtlpAnyValue([])).toEqual({ arrayValue: { values: [] } })
})
it('keeps a Date whose toISOString is overridden out of the wire format', () => {
const broken = new Date('2026-08-20T10:00:00.000Z')
;(broken as any).toISOString = () => ({})
expect(typeof toOtlpAnyValue(broken as unknown as LogAttributeValue).stringValue).toBe('string')
})
it('encodes sibling references to one object twice, not as circular', () => {
const shared = { id: 1 }
expect(toOtlpAnyValue({ a: shared, b: shared })).toEqual({
kvlistValue: {
values: [
{ key: 'a', value: { kvlistValue: { values: [{ key: 'id', value: { intValue: '1' } }] } } },
{ key: 'b', value: { kvlistValue: { values: [{ key: 'id', value: { intValue: '1' } }] } } },
],
},
})
})
})
describe('buildOtlpLogRecord attribute reads', () => {
// Reading `options.attributes` happens before the encoder's per-key guard.
it('marks an attribute whose getter throws without dropping the record', () => {
const attributes = {
ok: 1,
get bad(): number {
throw new Error('disposed store')
},
}
const record = buildOtlpLogRecord({ body: 'x', attributes }, {})
expect(record.attributes).toContainEqual({ key: 'ok', value: { intValue: '1' } })
expect(record.attributes).toContainEqual({ key: 'bad', value: { stringValue: '[Unserializable]' } })
})
// Plain assignment hits the prototype setter and the attribute vanishes.
it('keeps an attribute literally named __proto__', () => {
const attributes = JSON.parse('{"__proto__": {"a": 1}, "normal": "n"}')
const record = buildOtlpLogRecord({ body: 'x', attributes }, {})
expect(record.attributes.map((a) => a.key)).toContain('__proto__')
})
it('coerces a non-string body', () => {
// A non-string stringValue is refused for the whole request.
const record = buildOtlpLogRecord({ body: 12345 as unknown as string }, {})
expect(record.body).toEqual({ stringValue: '12345' })
})
// `String()` throws on these, and buildOtlpLogRecord is called straight from
// captureLog — beforeSend can also hand back any body.
it('marks a body that cannot be coerced to a string', () => {
const throwing = {
toString() {
throw new Error('boom')
},
} as unknown as string
expect(() => buildOtlpLogRecord({ body: throwing }, {})).not.toThrow()
expect(buildOtlpLogRecord({ body: throwing }, {}).body).toEqual({ stringValue: '[Unserializable]' })
expect(buildOtlpLogRecord({ body: Object.create(null) }, {}).body).toEqual({
stringValue: '[Unserializable]',
})
})
it('keeps the record when the attributes object itself cannot be read', () => {
const revocable = Proxy.revocable({ a: 1 }, {})
revocable.revoke()
expect(buildOtlpLogRecord({ body: 'x', attributes: revocable.proxy }, {}).body).toEqual({ stringValue: 'x' })
})
})
describe('toOtlpKeyValueList', () => {

@@ -136,3 +441,3 @@ it('converts a record to key-value list', () => {

{ key: 'name', value: { stringValue: 'test' } },
{ key: 'count', value: { intValue: 5 } },
{ key: 'count', value: { intValue: '5' } },
{ key: 'active', value: { boolValue: true } },

@@ -139,0 +444,0 @@ ])

@@ -12,4 +12,15 @@ import type {

} from '@posthog/types'
import type { Logger } from '../types'
import type { LogSdkContext, ResolvedPostHogLogsConfig } from './types'
import { isArray, isBoolean, isNull, isNullish, isUndefined } from '../utils'
import {
CIRCULAR_VALUE,
FUNCTION_VALUE,
MAX_JSON_SAFE_VALUE_DEPTH,
MAX_JSON_SAFE_VALUE_ITEMS,
MAX_JSON_SAFE_VALUE_NODES,
sanitizeString,
TRUNCATED_VALUE,
UNSERIALIZABLE_VALUE,
} from '../utils/json-utils'

@@ -43,13 +54,51 @@ // ============================================================================

export function toOtlpAnyValue(value: LogAttributeValue): OtlpAnyValue {
// 2^63 — one past int64 max.
const INT64_RANGE_LIMIT = 9223372036854775808
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
interface EncodeState {
/** Containers on the current path, so a back-reference becomes a marker. */
ancestors: WeakSet<object>
remainingNodes: number
}
function newState(): EncodeState {
return { ancestors: new WeakSet(), remainingNodes: MAX_JSON_SAFE_VALUE_NODES }
}
export function toOtlpAnyValue(value: LogAttributeValue, logger?: Logger): OtlpAnyValue {
try {
return encodeAnyValue(value, logger, newState(), 0)
} catch {
// Runs inside `captureLog` and the metrics flush: an error escaping here
// surfaces in the caller's own code.
return { stringValue: UNSERIALIZABLE_VALUE }
}
}
export function toOtlpKeyValueList(attrs: Record<string, LogAttributeValue>, logger?: Logger): OtlpKeyValue[] {
try {
return encodeKeyValueList(attrs, logger, newState(), 0)
} catch {
return []
}
}
function encodeAnyValue(
value: LogAttributeValue,
logger: Logger | undefined,
state: EncodeState,
depth: number
): OtlpAnyValue {
if (state.remainingNodes <= 0) {
return { stringValue: TRUNCATED_VALUE }
}
state.remainingNodes--
if (isBoolean(value)) {
return { boolValue: value }
}
// NOTE: typeof check (not core's isNumber) so NaN is included. core's
// isNumber explicitly excludes NaN via the `x === x` guard, which would
// route NaN through the JSON.stringify branch below — JSON has no
// representation for non-finite floats and JSON.stringify turns them into
// `null`, losing the value server-side. proto3 JSON mapping (which OTLP/HTTP
// rides) requires the literal strings; we encode them as stringValue to keep
// the human-readable signal regardless of which downstream parser sees them.
// typeof, not core's isNumber, which excludes NaN — proto3 JSON distinguishes
// a non-finite float from an ordinary string.
if (typeof value === 'number') {

@@ -60,3 +109,20 @@ if (!Number.isFinite(value)) {

if (Number.isInteger(value)) {
return { intValue: value }
if (Number.isSafeInteger(value)) {
return { intValue: String(value) }
}
// Past MAX_SAFE_INTEGER only BigInt gives the double's exact decimal:
// `String(-(2**63))` lands 192 below int64 min, outside the field it is
// about to be parsed into. Without BigInt the value rides as a string,
// which is never range-checked.
if (typeof BigInt === 'undefined') {
return { stringValue: String(value) }
}
const decimal = BigInt(value).toString()
if (value >= INT64_RANGE_LIMIT || value < -INT64_RANGE_LIMIT) {
// An out-of-range intValue 400s the whole logs request; on the metrics
// path it is swallowed server-side and the metric just disappears.
logger?.debug(`Attribute ${decimal} is outside the int64 range; encoding it as a string`)
return { stringValue: decimal }
}
return { intValue: decimal }
}

@@ -66,24 +132,112 @@ return { doubleValue: value }

if (typeof value === 'string') {
return { stringValue: value }
return { stringValue: sanitizeString(value) }
}
if (isArray(value)) {
return { arrayValue: { values: value.map((v) => toOtlpAnyValue(v as LogAttributeValue)) } }
// `String(value)` would put a function's source text on the wire.
if (typeof value === 'function') {
return { stringValue: FUNCTION_VALUE }
}
// Objects fall back to JSON. OTLP supports kvlistValue but the encoder
// stays flat for simplicity.
try {
return { stringValue: JSON.stringify(value) }
} catch {
if (typeof value === 'symbol') {
return { stringValue: String(value) }
}
if (typeof value === 'object' && value !== null) {
if (state.ancestors.has(value)) {
return { stringValue: CIRCULAR_VALUE }
}
if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) {
return { stringValue: TRUNCATED_VALUE }
}
if (value instanceof Date) {
const time = value.getTime()
const iso = Number.isFinite(time) ? value.toISOString() : String(value)
// An overridden toISOString can return a non-string, which the server
// refuses for the whole request.
return { stringValue: typeof iso === 'string' ? sanitizeString(iso) : String(iso) }
}
// Registered before the toJSON probe: a toJSON returning a structure that
// references its own object is a cycle like any other.
state.ancestors.add(value)
try {
// The representation a value defines for itself — dayjs, Decimal, an ORM
// document, and a cross-realm Date that fails the `instanceof` above.
try {
const toJSON = (value as { toJSON?: unknown }).toJSON
if (typeof toJSON === 'function') {
return encodeAnyValue(toJSON.call(value) as LogAttributeValue, logger, state, depth + 1)
}
} catch {
// A throwing toJSON falls through to the plain walk.
}
if (isArray(value)) {
return { arrayValue: { values: encodeArrayValues(value, logger, state, depth + 1) } }
}
return {
kvlistValue: {
values: encodeKeyValueList(value as Record<string, LogAttributeValue>, logger, state, depth + 1),
},
}
} finally {
// Siblings that reference the same object are duplication, not a cycle.
state.ancestors.delete(value)
}
}
return { stringValue: sanitizeString(String(value)) }
}
export function toOtlpKeyValueList(attrs: Record<string, LogAttributeValue>): OtlpKeyValue[] {
function encodeArrayValues(
values: unknown[],
logger: Logger | undefined,
state: EncodeState,
depth: number
): OtlpAnyValue[] {
const result: OtlpAnyValue[] = []
const itemCount = Math.min(values.length, MAX_JSON_SAFE_VALUE_ITEMS)
let index = 0
for (; index < itemCount && state.remainingNodes > 0; index++) {
try {
const element = index in values ? values[index] : undefined
// Dropped, as iOS and Android do: proto3 JSON has no null AnyValue, and
// both `null` and `{}` here are rejected for the whole request.
if (isNullish(element)) {
continue
}
result.push(encodeAnyValue(element as LogAttributeValue, logger, state, depth))
} catch {
result.push({ stringValue: UNSERIALIZABLE_VALUE })
}
}
if (values.length > index) {
result.push({ stringValue: TRUNCATED_VALUE })
}
return result
}
function encodeKeyValueList(
attrs: Record<string, LogAttributeValue>,
logger: Logger | undefined,
state: EncodeState,
depth: number
): OtlpKeyValue[] {
const result: OtlpKeyValue[] = []
for (const key in attrs) {
const value = attrs[key]
if (isNull(value) || isUndefined(value)) {
// for...in walks the prototype chain once own keys are exhausted. Skipped
// rather than broken out of: a proxy can yield keys in any order.
if (!propertyIsEnumerable.call(attrs, key)) {
continue
}
result.push({ key, value: toOtlpAnyValue(value) })
if (result.length >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
// Reported rather than written into the attributes: a synthetic key would
// land in the user's own namespace and could collide with a real one.
logger?.debug('Attributes truncated: the value exceeds the OTLP encoder budget')
break
}
try {
const value = attrs[key]
if (isNull(value) || isUndefined(value)) {
continue
}
result.push({ key: sanitizeString(key), value: encodeAnyValue(value, logger, state, depth) })
} catch {
// A getter that throws costs its own key, not the whole record.
result.push({ key: sanitizeString(key), value: { stringValue: UNSERIALIZABLE_VALUE } })
}
}

@@ -118,3 +272,18 @@ return result

*/
export function buildOtlpLogRecord(options: CaptureLogOptions, sdkContext: LogSdkContext): OtlpLogRecord {
// `body` is only typed as a string. An untyped caller — or a `beforeSend` that
// rewrites it — can pass anything, and `String()` throws on a value whose
// `toString` does, or on a null-prototype object.
function encodeBody(body: string): string {
try {
return sanitizeString(String(body))
} catch {
return UNSERIALIZABLE_VALUE
}
}
export function buildOtlpLogRecord(
options: CaptureLogOptions,
sdkContext: LogSdkContext,
logger?: Logger
): OtlpLogRecord {
const level: LogSeverityLevel = options.level || 'info'

@@ -154,5 +323,24 @@ const { text: severityText, number: severityNumber } = OTLP_SEVERITY_MAP[level] || DEFAULT_OTLP_SEVERITY

const mergedAttributes = {
...autoAttributes,
...(options.attributes || {}),
// Read key by key rather than spreading: a getter over a disposed store or a
// revoked proxy throws on the read itself, before the encoder's guards see it.
const mergedAttributes: Record<string, LogAttributeValue> = { ...autoAttributes }
const userAttributes = options.attributes
if (userAttributes) {
let keys: string[] = []
try {
keys = Object.keys(userAttributes)
} catch {
keys = []
}
for (const key of keys) {
let value: LogAttributeValue
try {
value = userAttributes[key]
} catch {
value = UNSERIALIZABLE_VALUE
}
// defineProperty, not assignment: `attributes['__proto__'] = v` hits the
// prototype setter and the attribute vanishes.
Object.defineProperty(mergedAttributes, key, { value, enumerable: true, writable: true, configurable: true })
}
}

@@ -165,4 +353,4 @@

severityText,
body: { stringValue: options.body },
attributes: toOtlpKeyValueList(mergedAttributes),
body: { stringValue: encodeBody(options.body) },
attributes: toOtlpKeyValueList(mergedAttributes, logger),
}

@@ -169,0 +357,0 @@

@@ -172,6 +172,20 @@ import type { Logger } from '../types'

expect(attrs).toContainEqual({ key: 'route', value: { stringValue: '/home' } })
expect(attrs).toContainEqual({ key: 'retries', value: { intValue: 2 } })
expect(attrs).toContainEqual({ key: 'retries', value: { intValue: '2' } })
expect(attrs).toContainEqual({ key: 'cached', value: { boolValue: true } })
})
// The encoder is shared with logs, so the debug line it emits on the
// out-of-range path only reaches anyone if metrics passes its logger down.
it('encodes an out-of-int64 attribute as a string and reports it through the logger', async () => {
const metrics = createMetrics()
metrics.count('api_calls', 1, { attributes: { huge: 2 ** 63 } as any })
await metrics.flush()
const attrs = sentMetrics()[0].sum!.dataPoints[0].attributes
expect(attrs).toContainEqual({ key: 'huge', value: { stringValue: '9223372036854775808' } })
expect((logger.debug as jest.Mock).mock.calls.some((c) => String(c[0]).includes('outside the int64 range'))).toBe(
true
)
})
it('stamps delta data points with a window: startTimeUnixNano <= timeUnixNano, both nano strings', async () => {

@@ -178,0 +192,0 @@ const metrics = createMetrics()

@@ -377,3 +377,3 @@ import type {

const attributes = toOtlpKeyValueList(state.attributes ?? {})
const attributes = toOtlpKeyValueList(state.attributes ?? {}, this._logger)
const startNano = msToUnixNano(state.windowStartMs)

@@ -380,0 +380,0 @@

@@ -6,3 +6,5 @@ import { FetchLike } from '../types'

export * from './bucketed-rate-limiter'
export * from './json-utils'
// Named rather than `export *`: the budgets, markers and `sanitizeString` are
// shared with the OTLP encoder but are not public API.
export { toJsonSafeValue } from './json-utils'
export * from './number-utils'

@@ -9,0 +11,0 @@ export * from './string-utils'

@@ -1,8 +0,10 @@

const MAX_JSON_SAFE_VALUE_DEPTH = 20
const MAX_JSON_SAFE_VALUE_ITEMS = 1_000
const MAX_JSON_SAFE_VALUE_NODES = 10_000
const CIRCULAR_VALUE = '[Circular]'
const TRUNCATED_VALUE = '[Truncated]'
const UNSERIALIZABLE_VALUE = '[Unserializable]'
const FUNCTION_VALUE = '[Function]'
/** Traversal budgets and markers, shared with the OTLP attribute encoder. Not
* public API — `utils/index.ts` re-exports `toJsonSafeValue` only. */
export const MAX_JSON_SAFE_VALUE_DEPTH = 20
export const MAX_JSON_SAFE_VALUE_ITEMS = 1_000
export const MAX_JSON_SAFE_VALUE_NODES = 10_000
export const CIRCULAR_VALUE = '[Circular]'
export const TRUNCATED_VALUE = '[Truncated]'
export const UNSERIALIZABLE_VALUE = '[Unserializable]'
export const FUNCTION_VALUE = '[Function]'

@@ -18,3 +20,8 @@ const dateGetTime = Date.prototype.getTime

function sanitizeString(value: string): string {
/**
* Replaces unpaired surrogates with U+FFFD. A lone surrogate survives
* `JSON.stringify` as a `\uD800`-style escape, which strict JSON parsers
* reject — for OTLP that means the whole request is refused.
*/
export function sanitizeString(value: string): string {
let output = ''

@@ -21,0 +28,0 @@ for (let index = 0; index < value.length; index++) {