123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- import * as Sentry from '@sentry/react';
- import partition from 'lodash/partition';
- import * as qs from 'query-string';
- import getThreadException from 'sentry/components/events/interfaces/threads/threadSelector/getThreadException';
- import {FILTER_MASK} from 'sentry/constants';
- import ConfigStore from 'sentry/stores/configStore';
- import type {Frame, PlatformKey, StacktraceType} from 'sentry/types';
- import type {Image} from 'sentry/types/debugImage';
- import type {EntryRequest, EntryThreads, Event, Thread} from 'sentry/types/event';
- import {EntryType} from 'sentry/types/event';
- import {defined} from 'sentry/utils';
- import {fileExtensionToPlatform, getFileExtension} from 'sentry/utils/fileExtension';
- /**
- * Attempts to escape a string from any bash double quote special characters.
- */
- function escapeBashString(v: string) {
- return v.replace(/(["$`\\])/g, '\\$1');
- }
- interface ImageForAddressProps {
- addrMode: Frame['addrMode'];
- address: Frame['instructionAddr'];
- event: Event;
- }
- interface HiddenFrameIndicesProps {
- data: StacktraceType;
- frameCountMap: {[frameIndex: number]: number};
- toggleFrameMap: {[frameIndex: number]: boolean};
- }
- export function findImageForAddress({event, addrMode, address}: ImageForAddressProps) {
- const images = event.entries.find(entry => entry.type === 'debugmeta')?.data?.images;
- if (!images || !address) {
- return null;
- }
- const image = images.find((img, idx) => {
- if (!addrMode || addrMode === 'abs') {
- const [startAddress, endAddress] = getImageRange(img);
- return address >= (startAddress as any) && address < (endAddress as any);
- }
- return addrMode === `rel:${idx}`;
- });
- return image;
- }
- export function isRepeatedFrame(frame: Frame, nextFrame?: Frame) {
- if (!nextFrame) {
- return false;
- }
- return (
- frame.lineNo === nextFrame.lineNo &&
- frame.instructionAddr === nextFrame.instructionAddr &&
- frame.package === nextFrame.package &&
- frame.module === nextFrame.module &&
- frame.function === nextFrame.function
- );
- }
- export function getRepeatedFrameIndices(data: StacktraceType) {
- const repeats: number[] = [];
- (data.frames ?? []).forEach((frame, frameIdx) => {
- const nextFrame = (data.frames ?? [])[frameIdx + 1];
- const repeatedFrame = isRepeatedFrame(frame, nextFrame);
- if (repeatedFrame) {
- repeats.push(frameIdx);
- }
- });
- return repeats;
- }
- export function getHiddenFrameIndices({
- data,
- toggleFrameMap,
- frameCountMap,
- }: HiddenFrameIndicesProps) {
- const repeatedIndeces = getRepeatedFrameIndices(data);
- let hiddenFrameIndices: number[] = [];
- Object.keys(toggleFrameMap)
- .filter(frameIndex => toggleFrameMap[frameIndex] === true)
- .forEach(indexString => {
- const index = parseInt(indexString, 10);
- const indicesToBeAdded: number[] = [];
- let i = 1;
- let numHidden = frameCountMap[index];
- while (numHidden > 0) {
- if (!repeatedIndeces.includes(index - i)) {
- indicesToBeAdded.push(index - i);
- numHidden -= 1;
- }
- i += 1;
- }
- hiddenFrameIndices = [...hiddenFrameIndices, ...indicesToBeAdded];
- });
- return hiddenFrameIndices;
- }
- export function getLastFrameIndex(frames: Frame[]) {
- const inAppFrameIndexes = frames
- .map((frame, frameIndex) => {
- if (frame.inApp) {
- return frameIndex;
- }
- return undefined;
- })
- .filter(frame => frame !== undefined);
- return !inAppFrameIndexes.length
- ? frames.length - 1
- : inAppFrameIndexes[inAppFrameIndexes.length - 1];
- }
- // TODO(dcramer): support cookies
- export function getCurlCommand(data: EntryRequest['data']) {
- let result = 'curl';
- if (defined(data.method) && data.method !== 'GET') {
- result += ' \\\n -X ' + data.method;
- }
- data.headers = data.headers?.filter(defined);
- // TODO(benvinegar): just gzip? what about deflate?
- const compressed = data.headers?.find(
- h => h[0] === 'Accept-Encoding' && h[1].includes('gzip')
- );
- if (compressed) {
- result += ' \\\n --compressed';
- }
- // sort headers
- const headers =
- data.headers?.sort(function (a, b) {
- return a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1;
- }) ?? [];
- for (const header of headers) {
- result += ' \\\n -H "' + header[0] + ': ' + escapeBashString(header[1] + '') + '"';
- }
- if (defined(data.data)) {
- switch (data.inferredContentType) {
- case 'application/json':
- result += ' \\\n --data "' + escapeBashString(JSON.stringify(data.data)) + '"';
- break;
- case 'application/x-www-form-urlencoded':
- result +=
- ' \\\n --data "' +
- escapeBashString(qs.stringify(data.data as {[key: string]: any})) +
- '"';
- break;
- default:
- if (typeof data.data === 'string') {
- result += ' \\\n --data "' + escapeBashString(data.data) + '"';
- } else if (Object.keys(data.data).length === 0) {
- // Do nothing with empty object data.
- } else {
- Sentry.withScope(scope => {
- scope.setExtra('data', data);
- Sentry.captureException(new Error('Unknown event data'));
- });
- }
- }
- }
- result += ' \\\n "' + getFullUrl(data) + '"';
- return result;
- }
- export function stringifyQueryList(query: string | [key: string, value: string][]) {
- if (typeof query === 'string') {
- return query;
- }
- const queryObj: Record<string, string[]> = {};
- for (const kv of query) {
- if (kv !== null && kv.length === 2) {
- const [key, value] = kv;
- if (value !== null) {
- if (Array.isArray(queryObj[key])) {
- queryObj[key].push(value);
- } else {
- queryObj[key] = [value];
- }
- }
- }
- }
- return qs.stringify(queryObj);
- }
- export function getFullUrl(data: EntryRequest['data']): string | undefined {
- let fullUrl = data?.url;
- if (!fullUrl) {
- return fullUrl;
- }
- if (data?.query?.length) {
- fullUrl += '?' + stringifyQueryList(data.query);
- }
- if (data.fragment) {
- fullUrl += '#' + data.fragment;
- }
- return escapeBashString(fullUrl);
- }
- /**
- * Converts an object of body/querystring key/value pairs
- * into a tuple of [key, value] pairs, and sorts them.
- *
- * This handles the case for query strings that were decoded like so:
- *
- * ?foo=bar&foo=baz => { foo: ['bar', 'baz'] }
- *
- * By converting them to [['foo', 'bar'], ['foo', 'baz']]
- */
- export function objectToSortedTupleArray(obj: Record<string, string | string[]>) {
- return Object.keys(obj)
- .reduce<[string, string][]>((out, k) => {
- const val = obj[k];
- return out.concat(
- Array.isArray(val)
- ? val.map(v => [k, v]) // key has multiple values (array)
- : ([[k, val]] as [string, string][]) // key has single value
- );
- }, [])
- .sort(function ([keyA, valA], [keyB, valB]) {
- // if keys are identical, sort on value
- if (keyA === keyB) {
- return valA < valB ? -1 : 1;
- }
- return keyA < keyB ? -1 : 1;
- });
- }
- // for context summaries and avatars
- export function removeFilterMaskedEntries<T extends Record<string, any>>(rawData: T): T {
- const cleanedData: Record<string, any> = {};
- for (const key of Object.getOwnPropertyNames(rawData)) {
- if (rawData[key] !== FILTER_MASK) {
- cleanedData[key] = rawData[key];
- }
- }
- return cleanedData as T;
- }
- export function formatAddress(address: number, imageAddressLength: number | undefined) {
- return `0x${address.toString(16).padStart(imageAddressLength ?? 0, '0')}`;
- }
- export function parseAddress(address?: string | null) {
- if (!address) {
- return 0;
- }
- try {
- return parseInt(address, 16) || 0;
- } catch (_e) {
- return 0;
- }
- }
- export function getImageRange(image: Image) {
- // The start address is normalized to a `0x` prefixed hex string. The event
- // schema also allows ingesting plain numbers, but this is converted during
- // ingestion.
- const startAddress = parseAddress(image?.image_addr);
- // The image size is normalized to a regular number. However, it can also be
- // `null`, in which case we assume that it counts up to the next image.
- const endAddress = startAddress + (image?.image_size || 0);
- return [startAddress, endAddress];
- }
- export function parseAssembly(assembly: string | null) {
- let name: string | undefined;
- let version: string | undefined;
- let culture: string | undefined;
- let publicKeyToken: string | undefined;
- const pieces = assembly ? assembly.split(',') : [];
- if (pieces.length > 0) {
- name = pieces[0];
- }
- for (let i = 1; i < pieces.length; i++) {
- const [key, value] = pieces[i].trim().split('=');
- // eslint-disable-next-line default-case
- switch (key) {
- case 'Version':
- version = value;
- break;
- case 'Culture':
- if (value !== 'neutral') {
- culture = value;
- }
- break;
- case 'PublicKeyToken':
- if (value !== 'null') {
- publicKeyToken = value;
- }
- break;
- }
- }
- return {name, version, culture, publicKeyToken};
- }
- function getFramePlatform(frame: Frame) {
- const fileExtension = getFileExtension(frame.filename ?? '');
- const fileExtensionPlatform = fileExtension
- ? fileExtensionToPlatform(fileExtension)
- : null;
- if (fileExtensionPlatform) {
- return fileExtensionPlatform;
- }
- if (frame.platform) {
- return frame.platform;
- }
- return null;
- }
- /**
- * Returns the representative platform for the given stack trace frames.
- * Prioritizes recent in-app frames, checking first for a matching file extension
- * and then for a frame.platform attribute [1].
- *
- * If none of the frames have a platform, falls back to the event platform.
- *
- * [1] https://develop.sentry.dev/sdk/event-payloads/stacktrace/#frame-attributes
- */
- export function stackTracePlatformIcon(eventPlatform: PlatformKey, frames: Frame[]) {
- const [inAppFrames, systemFrames] = partition(
- // Reverse frames to get newest-first ordering
- [...frames].reverse(),
- frame => frame.inApp
- );
- for (const frame of [...inAppFrames, ...systemFrames]) {
- const framePlatform = getFramePlatform(frame);
- if (framePlatform) {
- return framePlatform;
- }
- }
- return eventPlatform;
- }
- export function isStacktraceNewestFirst() {
- const user = ConfigStore.get('user');
- // user may not be authenticated
- if (!user) {
- return true;
- }
- switch (user.options.stacktraceOrder) {
- case 2:
- return true;
- case 1:
- return false;
- case -1:
- default:
- return true;
- }
- }
- export function getCurrentThread(event: Event) {
- const threads = event.entries?.find(entry => entry.type === EntryType.THREADS) as
- | EntryThreads
- | undefined;
- return threads?.data.values?.find(thread => thread.current);
- }
- export function getThreadById(event: Event, tid?: number) {
- const threads = event.entries?.find(entry => entry.type === EntryType.THREADS) as
- | EntryThreads
- | undefined;
- return threads?.data.values?.find(thread => thread.id === tid);
- }
- export function getStacktracePlatform(
- event: Event,
- stacktrace?: StacktraceType | null
- ): PlatformKey {
- const overridePlatform = stacktrace?.frames?.find(frame =>
- defined(frame.platform)
- )?.platform;
- return overridePlatform ?? event.platform ?? 'other';
- }
- export function inferPlatform(event: Event, thread?: Thread): PlatformKey {
- const exception = getThreadException(event, thread);
- let exceptionFramePlatform: Frame | undefined = undefined;
- for (const value of exception?.values ?? []) {
- exceptionFramePlatform = value.stacktrace?.frames?.find(frame => !!frame.platform);
- if (exceptionFramePlatform) {
- break;
- }
- }
- if (exceptionFramePlatform?.platform) {
- return exceptionFramePlatform.platform;
- }
- const threadFramePlatform = thread?.stacktrace?.frames?.find(frame => !!frame.platform);
- if (threadFramePlatform?.platform) {
- return threadFramePlatform.platform;
- }
- return event.platform ?? 'other';
- }
|