posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
320 lines • 17 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostHogExceptions = void 0;
exports.buildErrorPropertiesBuilder = buildErrorPropertiesBuilder;
var constants_1 = require("./constants");
var logger_1 = require("@posthog/browser-common/utils/logger");
var property_utils_1 = require("@posthog/browser-common/utils/property-utils");
var core_1 = require("@posthog/core");
var stack_overflow_1 = require("./utils/stack-overflow");
var logger = (0, logger_1.createLogger)('[Error tracking]');
// Safari masks the URL of extension content scripts as `webkit-masked-url://hidden/`. It masks
// some of the page's own scripts the same way, so a masked frame alone does not prove the
// exception came from an extension (see _isExtensionException).
var MASKED_URL_PREFIX = 'webkit-masked-url:';
var MASKED_EXTENSION_EXCEPTION_VALUE = [
{ value: 'isolatedAPI.contexts.topHostname', exact: false },
{ value: 'No response from target', exact: true },
];
// Browser extensions serve their content scripts from these schemes. `safari-extension:` and
// `safari-web-extension:` are synthesised by the stack parser (see extractSafariExtensionDetails)
// rather than being real URLs, but they mark the frame just as definitively.
var EXTENSION_URL_PREFIXES = [
'chrome-extension://',
'moz-extension://',
'safari-extension:',
'safari-web-extension:',
MASKED_URL_PREFIX,
];
// Some mobile browsers inject their own user scripts into every page. These scripts read
// browser-private globals, and when a script runs before its global exists it throws. The
// browser attributes the injected script to the host document, so the frame filename is the
// page URL and EXTENSION_URL_PREFIXES cannot catch it. We match the private global in the
// exception value instead. No page or SDK code references these names.
var INJECTED_BROWSER_SCRIPT_GLOBALS = ['__firefox__', '__gCrWeb'];
function buildErrorPropertiesBuilder() {
return new core_1.ErrorTracking.ErrorPropertiesBuilder([
new core_1.ErrorTracking.DOMExceptionCoercer(),
new core_1.ErrorTracking.PromiseRejectionEventCoercer(),
new core_1.ErrorTracking.ErrorEventCoercer(),
new core_1.ErrorTracking.ErrorCoercer(),
new core_1.ErrorTracking.EventCoercer(),
new core_1.ErrorTracking.ObjectCoercer(),
new core_1.ErrorTracking.StringCoercer(),
new core_1.ErrorTracking.PrimitiveCoercer(),
], core_1.ErrorTracking.createDefaultStackParser());
}
var PostHogExceptions = /** @class */ (function () {
function PostHogExceptions(instance) {
var _a, _b;
this._suppressionRules = [];
this._errorPropertiesBuilder = buildErrorPropertiesBuilder();
this._instance = instance;
this._suppressionRules = (_b = (_a = this._instance.persistence) === null || _a === void 0 ? void 0 : _a.get_property(constants_1.ERROR_TRACKING_SUPPRESSION_RULES)) !== null && _b !== void 0 ? _b : [];
this._exceptionStepsConfig = core_1.ErrorTracking.resolveExceptionStepsConfig(this._getExceptionStepsConfig());
this._exceptionStepsBuffer = new core_1.ErrorTracking.ExceptionStepsBuffer(this._exceptionStepsConfig);
}
PostHogExceptions.prototype.onConfigChange = function () {
this._exceptionStepsConfig = core_1.ErrorTracking.resolveExceptionStepsConfig(this._getExceptionStepsConfig());
this._exceptionStepsBuffer.setConfig(this._exceptionStepsConfig);
};
PostHogExceptions.prototype.onRemoteConfig = function (result) {
var _a;
var _b, _c, _d;
if (!result.ok) {
// Failure behaves like a response without an errorTracking key.
return;
}
var response = result.config;
if (!('errorTracking' in response)) {
return;
}
var suppressionRules = (_c = (_b = response.errorTracking) === null || _b === void 0 ? void 0 : _b.suppressionRules) !== null && _c !== void 0 ? _c : [];
var captureExtensionExceptions = (_d = response.errorTracking) === null || _d === void 0 ? void 0 : _d.captureExtensionExceptions;
// store this in-memory in case persistence is disabled
this._suppressionRules = suppressionRules;
if (this._instance.persistence) {
this._instance.persistence.register((_a = {},
_a[constants_1.ERROR_TRACKING_SUPPRESSION_RULES] = this._suppressionRules,
_a[constants_1.ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS] = captureExtensionExceptions,
_a));
}
};
Object.defineProperty(PostHogExceptions.prototype, "_captureExtensionExceptions", {
get: function () {
var _a;
var enabled_server_side = !!this._instance.get_property(constants_1.ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS);
var enabled_client_side = this._instance.config.error_tracking.captureExtensionExceptions;
return (_a = enabled_client_side !== null && enabled_client_side !== void 0 ? enabled_client_side : enabled_server_side) !== null && _a !== void 0 ? _a : false;
},
enumerable: false,
configurable: true
});
PostHogExceptions.prototype.buildProperties = function (input, metadata) {
return this._errorPropertiesBuilder.buildFromUnknown(input, {
syntheticException: metadata === null || metadata === void 0 ? void 0 : metadata.syntheticException,
mechanism: {
handled: metadata === null || metadata === void 0 ? void 0 : metadata.handled,
},
});
};
PostHogExceptions.prototype.addExceptionStep = function (message, properties) {
var _a;
if (!this._exceptionStepsConfig.enabled) {
return;
}
try {
if (!(0, core_1.isString)(message) || message.trim().length === 0) {
logger.warn('Ignoring exception step because message must be a non-empty string');
return;
}
var userProperties = this._coerceExceptionStepProperties(properties);
var _b = core_1.ErrorTracking.stripReservedExceptionStepFields(userProperties), sanitizedProperties = _b.sanitizedProperties, droppedKeys = _b.droppedKeys;
if (droppedKeys.length > 0) {
logger.warn('Ignoring reserved exception step fields', { droppedKeys: droppedKeys });
}
this._exceptionStepsBuffer.add(__assign((_a = {}, _a[core_1.ErrorTracking.EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE] = message, _a[core_1.ErrorTracking.EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP] = new Date().toISOString(), _a), sanitizedProperties));
}
catch (error) {
logger.error('Failed to add exception step. Ignoring breadcrumb.', error);
}
};
PostHogExceptions.prototype.sendExceptionEvent = function (properties) {
try {
var exceptionList = properties.$exception_list;
if (this._isExceptionList(exceptionList)) {
if (this._matchesSuppressionRule(exceptionList)) {
this._addDroppedExceptionStep('Exception dropped: matched a suppression rule');
logger.info('Skipping exception capture because a suppression rule matched');
return;
}
if (!this._captureExtensionExceptions && this._isExtensionException(exceptionList)) {
this._addDroppedExceptionStep('Exception dropped: thrown by a browser extension');
logger.info('Skipping exception capture because it was thrown by an extension');
return;
}
if (!this._captureExtensionExceptions && this._isInjectedBrowserScriptException(exceptionList)) {
this._addDroppedExceptionStep('Exception dropped: thrown by an injected browser script');
logger.info('Skipping exception capture because it was thrown by an injected browser script');
return;
}
if (!this._instance.config.error_tracking.__capturePostHogExceptions &&
this._isPostHogException(exceptionList)) {
this._addDroppedExceptionStep('Exception dropped: thrown by the PostHog SDK');
logger.info('Skipping exception capture because it was thrown by the PostHog SDK');
return;
}
}
var propertiesForExceptionCapture = this._exceptionStepsConfig.enabled && (0, core_1.isNullish)(properties.$exception_steps)
? this._addBufferedExceptionSteps(properties)
: properties;
var injectedReleaseId = core_1.ErrorTracking.getInjectedReleaseId();
if (injectedReleaseId) {
propertiesForExceptionCapture.$release_id = injectedReleaseId;
}
try {
var result = this._instance.capture('$exception', propertiesForExceptionCapture, {
_noTruncate: true,
_batchKey: 'exceptionEvent',
_originatedFromCaptureException: true,
});
if (result) {
this._exceptionStepsBuffer.clear();
}
return result;
}
catch (error) {
if (!(0, stack_overflow_1.isStackOverflowError)(error)) {
logger.error('Failed to capture exception event. Dropping this exception.', error);
}
this._exceptionStepsBuffer.clear();
return;
}
}
catch (error) {
if (!(0, stack_overflow_1.isStackOverflowError)(error)) {
logger.error('Failed to process exception event. Ignoring this exception.', error);
}
return;
}
};
PostHogExceptions.prototype._addBufferedExceptionSteps = function (properties) {
try {
var exceptionSteps = this._exceptionStepsBuffer.getAttachable();
if (exceptionSteps.length === 0) {
return properties;
}
return __assign(__assign({}, properties), { $exception_steps: exceptionSteps });
}
catch (error) {
logger.error('Failed to read buffered exception steps. Capturing exception without steps.', error);
return properties;
}
};
PostHogExceptions.prototype._addDroppedExceptionStep = function (message) {
var _a;
if (this._exceptionStepsConfig.enabled) {
this._exceptionStepsBuffer.add((_a = {},
_a[core_1.ErrorTracking.EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE] = message,
_a[core_1.ErrorTracking.EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP] = new Date().toISOString(),
_a));
}
};
PostHogExceptions.prototype._coerceExceptionStepProperties = function (properties) {
if (!(0, core_1.isObject)(properties)) {
return {};
}
return __assign({}, properties);
};
PostHogExceptions.prototype._getExceptionStepsConfig = function () {
var _a, _b;
return (_b = (_a = this._instance.config.error_tracking) === null || _a === void 0 ? void 0 : _a.exception_steps) !== null && _b !== void 0 ? _b : {};
};
PostHogExceptions.prototype._matchesSuppressionRule = function (exceptionList) {
if (exceptionList.length === 0) {
return false;
}
try {
var exceptionValues_1 = exceptionList.reduce(function (acc, _a) {
var type = _a.type, value = _a.value;
if ((0, core_1.isString)(type) && type.length > 0) {
acc['$exception_types'].push(type);
}
if ((0, core_1.isString)(value) && value.length > 0) {
acc['$exception_values'].push(value);
}
return acc;
}, {
$exception_types: [],
$exception_values: [],
});
return this._suppressionRules.some(function (rule) {
var results = rule.values.map(function (v) {
var compare = property_utils_1.propertyComparisons[v.operator];
var values = exceptionValues_1[v.key];
if (!compare || !values) {
return false;
}
var targets = (0, core_1.isArray)(v.value) ? v.value : [v.value];
return targets.length > 0 ? compare(targets, values) : false;
});
return rule.type === 'OR' ? results.some(Boolean) : results.every(Boolean);
});
}
catch (error) {
// Suppression only ever filters what we capture, so failing to evaluate it must not cost
// us the exception. Letting this throw reaches the handler in sendExceptionEvent, which
// drops the event entirely.
logger.warn('Failed to evaluate suppression rules. Capturing the exception.', error);
return false;
}
};
PostHogExceptions.prototype._isExtensionException = function (exceptionList) {
var frames = exceptionList.flatMap(function (e) { var _a, _b; return (_b = (_a = e.stacktrace) === null || _a === void 0 ? void 0 : _a.frames) !== null && _b !== void 0 ? _b : []; });
var extensionFrames = frames.filter(function (_a) {
var filename = _a.filename;
return !!filename && EXTENSION_URL_PREFIXES.some(function (prefix) { return filename.startsWith(prefix); });
});
if (extensionFrames.length === 0) {
return false;
}
// Safari also masks blob, eval'd, and injected application code, so a masked URL alone does
// not prove the exception came from an extension. For an all-masked stack, require a known
// extension-only signature and no remaining page frame. Ignore masked frames when checking
// for page code because the Sentry integration may forward them with `in_app: true`.
var onlyMaskedExtensionFrames = extensionFrames.every(function (_a) {
var filename = _a.filename;
return !!filename && filename.startsWith(MASKED_URL_PREFIX);
});
if (onlyMaskedExtensionFrames) {
var hasKnownExtensionSignature = exceptionList.some(function (_a) {
var type = _a.type, value = _a.value;
return type === 'NoResponse' ||
((0, core_1.isString)(value) &&
MASKED_EXTENSION_EXCEPTION_VALUE.some(function (_a) {
var signature = _a.value, exact = _a.exact;
return exact ? value === signature : value.includes(signature);
}));
});
return (hasKnownExtensionSignature &&
!frames.some(function (_a) {
var in_app = _a.in_app, filename = _a.filename;
return in_app && !(filename === null || filename === void 0 ? void 0 : filename.startsWith(MASKED_URL_PREFIX));
}));
}
return true;
};
PostHogExceptions.prototype._isInjectedBrowserScriptException = function (exceptionList) {
return exceptionList.some(function (_a) {
var value = _a.value;
return (0, core_1.isString)(value) && INJECTED_BROWSER_SCRIPT_GLOBALS.some(function (global) { return value.includes(global); });
});
};
PostHogExceptions.prototype._isPostHogException = function (exceptionList) {
var _a, _b, _c, _d;
if (exceptionList.length > 0) {
var exception = exceptionList[0];
var frames_1 = (_b = (_a = exception.stacktrace) === null || _a === void 0 ? void 0 : _a.frames) !== null && _b !== void 0 ? _b : [];
var lastFrame = frames_1[frames_1.length - 1];
return (_d = (_c = lastFrame === null || lastFrame === void 0 ? void 0 : lastFrame.filename) === null || _c === void 0 ? void 0 : _c.includes('posthog.com/static')) !== null && _d !== void 0 ? _d : false;
}
return false;
};
PostHogExceptions.prototype._isExceptionList = function (candidate) {
return !(0, core_1.isNullish)(candidate) && (0, core_1.isArray)(candidate);
};
return PostHogExceptions;
}());
exports.PostHogExceptions = PostHogExceptions;
//# sourceMappingURL=posthog-exceptions.js.map