posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
206 lines • 10.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SEVEN_MEGABYTES = exports.CONSOLE_LOG_PLUGIN_NAME = exports.MAX_MESSAGE_SIZE = exports.MUTATION_SOURCE_TYPE = exports.PLUGIN_EVENT_TYPE = exports.INCREMENTAL_SNAPSHOT_EVENT_TYPE = exports.META_EVENT_TYPE = exports.FULL_SNAPSHOT_EVENT_TYPE = exports.replacementImageURI = exports.UNSTRINGIFIABLE_EVENT_SIZE = void 0;
exports.circularReferenceReplacer = circularReferenceReplacer;
exports.estimateSize = estimateSize;
exports.estimateCompressedEventSize = estimateCompressedEventSize;
exports.ensureMaxMessageSize = ensureMaxMessageSize;
exports.truncateLargeConsoleLogs = truncateLargeConsoleLogs;
exports.splitBuffer = splitBuffer;
var core_1 = require("@posthog/core");
// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
function circularReferenceReplacer() {
var ancestors = [];
return function (_key, value) {
// arrays must be tracked as ancestors too - isObject excludes them, and an
// untracked array in the path empties the ancestor stack, so a cycle passing
// through an array would be missed and JSON.stringify would throw
if ((0, core_1.isObject)(value) || (0, core_1.isArray)(value)) {
// `this` is the object that value is contained in,
// i.e., its direct parent.
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
ancestors.pop();
}
if (ancestors.includes(value)) {
return '[Circular]';
}
ancestors.push(value);
return value;
}
else {
return value;
}
};
}
function estimateStringBytes(data) {
return new Blob([data]).size;
}
// an event whose JSON is longer than the engine's maximum string length makes JSON.stringify
// throw `RangeError: Invalid string length`. Such an event can never reach the server either -
// the request encoder stringifies the whole batch - so estimateSize reports the failure with
// this size instead of throwing, and the caller drops that one event.
exports.UNSTRINGIFIABLE_EVENT_SIZE = -1;
function estimateSize(sizeable) {
try {
var stringifiedData = JSON.stringify(sizeable, circularReferenceReplacer());
return stringifiedData ? estimateStringBytes(stringifiedData) : 0;
}
catch (_a) {
return exports.UNSTRINGIFIABLE_EVENT_SIZE;
}
}
// Lightweight size estimate for compressed events without allocating a JSON string.
// Intentionally loose: does not account for JSON escaping of strings or keys.
// This is fine because compressed event strings are base64 gzip output (safe alphabet)
// and keys are known-safe identifiers. Used only for buffer threshold checks (~1MB)
// and split-buffer decisions (~7MB) where a few bytes of drift don't matter.
function estimateCompressedEventSize(value) {
if ((0, core_1.isNull)(value)) {
return 4;
}
if ((0, core_1.isUndefined)(value)) {
return 0;
}
switch (typeof value) {
case 'string':
return value.length + 2;
case 'number':
return String(value).length;
case 'boolean':
return value ? 4 : 5;
case 'object': {
if ((0, core_1.isArray)(value)) {
var size_1 = 2;
for (var i = 0; i < value.length; i++) {
if (i > 0)
size_1 += 1;
var el = value[i];
size_1 += (0, core_1.isUndefined)(el) || (0, core_1.isNull)(el) ? 4 : estimateCompressedEventSize(el);
}
return size_1;
}
var obj = value;
var size = 2;
var first = true;
for (var key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key))
continue;
var val = obj[key];
if ((0, core_1.isUndefined)(val))
continue;
if (!first)
size += 1;
first = false;
size += key.length + 3 + estimateCompressedEventSize(val);
}
return size;
}
default:
return 0;
}
}
exports.replacementImageURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNOCAwSDE2TDAgMTZWOEw4IDBaIiBmaWxsPSIjMkQyRDJEIi8+CjxwYXRoIGQ9Ik0xNiA4VjE2SDhMMTYgOFoiIGZpbGw9IiMyRDJEMkQiLz4KPC9zdmc+Cg==';
exports.FULL_SNAPSHOT_EVENT_TYPE = 2;
exports.META_EVENT_TYPE = 4;
exports.INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
exports.PLUGIN_EVENT_TYPE = 6;
exports.MUTATION_SOURCE_TYPE = 0;
exports.MAX_MESSAGE_SIZE = 5000000; // ~5mb
/*
* Check whether a data payload is nearing 5mb. If it is, it checks the data for
* data URIs (the likely culprit for large payloads). If it finds data URIs, it either replaces
* it with a generic image (if it's an image) or removes it.
* @data {object} the rr-web data object
* @returns {object} the rr-web data object with data uris filtered out
*/
function ensureMaxMessageSize(data) {
var stringifiedData = JSON.stringify(data);
// Note: with compression, this limit may be able to be increased
// but we're assuming most of the size is from a data uri which
// is unlikely to be compressed further
var size = estimateStringBytes(stringifiedData);
if (size > exports.MAX_MESSAGE_SIZE) {
// Regex that matches the pattern for a dataURI with the shape 'data:{mime type}[;{param}[={value}]]*,{data}'. It:
// 1) Checks if the pattern starts with 'data:' (potentially, not at the start of the string)
// 2) Extracts the mime type of the data uri in the first group (e.g. image/svg+xml)
// 3) Allows any number of parameters, e.g. ';base64', ';charset=utf-8;base64' or none
// 4) Determines when the data URI ends. It is matched against JSON text, so it must never consume
// a " or \ (a quote inside a string is escaped as \", and consuming part of an escape would
// produce invalid JSON). It also stops at ), ' and whitespace, which end a URI in css or srcset.
var dataURIRegex = /data:([\w/\-.+]+)(?:;[\w\-.+]+(?:=[\w\-.+%]+)?)*,[^)"'\s\\]*/gim;
var unfilteredStringifiedData = stringifiedData;
stringifiedData = stringifiedData.replace(dataURIRegex, function (_match, mimeType) {
return mimeType.toLocaleLowerCase().slice(0, 6) === 'image/' ? exports.replacementImageURI : '';
});
if (stringifiedData !== unfilteredStringifiedData) {
size = estimateStringBytes(stringifiedData);
}
}
return { event: JSON.parse(stringifiedData), size: size };
}
exports.CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1'; // The name of the rr-web plugin that emits console logs
// Console logs can be really large. This function truncates large logs
// It's a simple function that just truncates long strings.
// TODO: Ideally this function would have better handling of objects + lists,
// so they could still be rendered in a pretty way after truncation.
function truncateLargeConsoleLogs(_event) {
var event = _event;
var MAX_STRING_SIZE = 2000; // Maximum number of characters allowed in a string
var MAX_STRINGS_PER_LOG = 10; // A log can consist of multiple strings (e.g. consol.log('string1', 'string2'))
if (event &&
(0, core_1.isObject)(event) &&
event.type === exports.PLUGIN_EVENT_TYPE &&
(0, core_1.isObject)(event.data) &&
event.data.plugin === exports.CONSOLE_LOG_PLUGIN_NAME) {
// Note: event.data.payload.payload comes from rr-web, and is an array of strings
if (event.data.payload.payload.length > MAX_STRINGS_PER_LOG) {
event.data.payload.payload = event.data.payload.payload.slice(0, MAX_STRINGS_PER_LOG);
event.data.payload.payload.push('...[truncated]');
}
var updatedPayload = [];
for (var i = 0; i < event.data.payload.payload.length; i++) {
if (event.data.payload.payload[i] && // Value can be null
event.data.payload.payload[i].length > MAX_STRING_SIZE) {
updatedPayload.push(event.data.payload.payload[i].slice(0, MAX_STRING_SIZE) + '...[truncated]');
}
else {
updatedPayload.push(event.data.payload.payload[i]);
}
}
event.data.payload.payload = updatedPayload;
// Return original type
return _event;
}
return _event;
}
exports.SEVEN_MEGABYTES = 1024 * 1024 * 7 * 0.9; // ~7mb (with some wiggle room)
// recursively splits large buffers into smaller ones
// uses a pretty high size limit to avoid splitting too much
function splitBuffer(buffer, sizeLimit) {
if (sizeLimit === void 0) { sizeLimit = exports.SEVEN_MEGABYTES; }
if (buffer.size >= sizeLimit && buffer.data.length > 1) {
var half = Math.floor(buffer.data.length / 2);
var firstHalfSizes = buffer.sizes.slice(0, half);
var secondHalfSizes = buffer.sizes.slice(half);
return [
splitBuffer({
size: firstHalfSizes.reduce(function (a, b) { return a + b; }, 0),
data: buffer.data.slice(0, half),
sizes: firstHalfSizes,
sessionId: buffer.sessionId,
windowId: buffer.windowId,
}),
splitBuffer({
size: secondHalfSizes.reduce(function (a, b) { return a + b; }, 0),
data: buffer.data.slice(half),
sizes: secondHalfSizes,
sessionId: buffer.sessionId,
windowId: buffer.windowId,
}),
].flatMap(function (x) { return x; });
}
else {
return [buffer];
}
}
//# sourceMappingURL=sessionrecording-utils.js.map