Skip to content

Commit 6627882

Browse files
authored
feat(util): Add stringMatchesSomePattern helper function. (#6205)
This adds a new utility function, `stringMatchesSomePattern`, which tests a string against an entire array of strings and/or regexes at once (which is something we do in a number of spots in the SDK). It also adds an optional parameter to `isMatchingPattern` to allow the string matching to be exact rather than a substring match.
1 parent 23c19bf commit 6627882

File tree

5 files changed

+105
-21
lines changed

5 files changed

+105
-21
lines changed

packages/core/src/integrations/inboundfilters.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types';
2-
import { getEventDescription, isMatchingPattern, logger } from '@sentry/utils';
2+
import { getEventDescription, logger, stringMatchesSomePattern } from '@sentry/utils';
33

44
// "Script error." is hard coded into browsers for errors that it can't read.
55
// this is the result of a script being pulled in from an external domain and CORS.
@@ -107,9 +107,7 @@ function _isIgnoredError(event: Event, ignoreErrors?: Array<string | RegExp>): b
107107
return false;
108108
}
109109

110-
return _getPossibleEventMessages(event).some(message =>
111-
ignoreErrors.some(pattern => isMatchingPattern(message, pattern)),
112-
);
110+
return _getPossibleEventMessages(event).some(message => stringMatchesSomePattern(message, ignoreErrors));
113111
}
114112

115113
function _isDeniedUrl(event: Event, denyUrls?: Array<string | RegExp>): boolean {
@@ -118,7 +116,7 @@ function _isDeniedUrl(event: Event, denyUrls?: Array<string | RegExp>): boolean
118116
return false;
119117
}
120118
const url = _getEventFilterUrl(event);
121-
return !url ? false : denyUrls.some(pattern => isMatchingPattern(url, pattern));
119+
return !url ? false : stringMatchesSomePattern(url, denyUrls);
122120
}
123121

124122
function _isAllowedUrl(event: Event, allowUrls?: Array<string | RegExp>): boolean {
@@ -127,7 +125,7 @@ function _isAllowedUrl(event: Event, allowUrls?: Array<string | RegExp>): boolea
127125
return true;
128126
}
129127
const url = _getEventFilterUrl(event);
130-
return !url ? true : allowUrls.some(pattern => isMatchingPattern(url, pattern));
128+
return !url ? true : stringMatchesSomePattern(url, allowUrls);
131129
}
132130

133131
function _getPossibleEventMessages(event: Event): string[] {

packages/node/src/integrations/http.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { EventProcessor, Integration, Span, TracePropagationTargets } from '@sen
33
import {
44
dynamicSamplingContextToSentryBaggageHeader,
55
fill,
6-
isMatchingPattern,
76
logger,
87
parseSemver,
8+
stringMatchesSomePattern,
99
} from '@sentry/utils';
1010
import * as http from 'http';
1111
import * as https from 'https';
@@ -169,9 +169,7 @@ function _createWrappedRequestMethodFactory(
169169
return headersUrlMap[url];
170170
}
171171

172-
headersUrlMap[url] = tracingOptions.tracePropagationTargets.some(tracePropagationTarget =>
173-
isMatchingPattern(url, tracePropagationTarget),
174-
);
172+
headersUrlMap[url] = stringMatchesSomePattern(url, tracingOptions.tracePropagationTargets);
175173

176174
return headersUrlMap[url];
177175
};

packages/tracing/src/browser/request.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
BAGGAGE_HEADER_NAME,
66
dynamicSamplingContextToSentryBaggageHeader,
77
isInstanceOf,
8-
isMatchingPattern,
8+
stringMatchesSomePattern,
99
} from '@sentry/utils';
1010

1111
import { getActiveTransaction, hasTracingEnabled } from '../utils';
@@ -123,8 +123,7 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
123123
typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_: string) => true;
124124

125125
const shouldAttachHeaders = (url: string): boolean =>
126-
tracingOrigins.some(origin => isMatchingPattern(url, origin)) ||
127-
tracePropagationTargets.some(origin => isMatchingPattern(url, origin));
126+
stringMatchesSomePattern(url, tracingOrigins) || stringMatchesSomePattern(url, tracePropagationTargets);
128127

129128
const spans: Record<string, Span> = {};
130129

packages/utils/src/string.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,50 @@ export function safeJoin(input: any[], delimiter?: string): string {
8484
}
8585

8686
/**
87-
* Checks if the value matches a regex or includes the string
88-
* @param value The string value to be checked against
89-
* @param pattern Either a regex or a string that must be contained in value
87+
* Checks if the given value matches a regex or string
88+
*
89+
* @param value The string to test
90+
* @param pattern Either a regex or a string against which `value` will be matched
91+
* @param requireExactStringMatch If true, `value` must match `pattern` exactly. If false, `value` will match
92+
* `pattern` if it contains `pattern`. Only applies to string-type patterns.
9093
*/
91-
export function isMatchingPattern(value: string, pattern: RegExp | string): boolean {
94+
export function isMatchingPattern(
95+
value: string,
96+
pattern: RegExp | string,
97+
requireExactStringMatch: boolean = false,
98+
): boolean {
9299
if (!isString(value)) {
93100
return false;
94101
}
95102

96103
if (isRegExp(pattern)) {
97104
return pattern.test(value);
98105
}
99-
if (typeof pattern === 'string') {
100-
return value.indexOf(pattern) !== -1;
106+
if (isString(pattern)) {
107+
return requireExactStringMatch ? value === pattern : value.includes(pattern);
101108
}
109+
102110
return false;
103111
}
104112

113+
/**
114+
* Test the given string against an array of strings and regexes. By default, string matching is done on a
115+
* substring-inclusion basis rather than a strict equality basis
116+
*
117+
* @param testString The string to test
118+
* @param patterns The patterns against which to test the string
119+
* @param requireExactStringMatch If true, `testString` must match one of the given string patterns exactly in order to
120+
* count. If false, `testString` will match a string pattern if it contains that pattern.
121+
* @returns
122+
*/
123+
export function stringMatchesSomePattern(
124+
testString: string,
125+
patterns: Array<string | RegExp> = [],
126+
requireExactStringMatch: boolean = false,
127+
): boolean {
128+
return patterns.some(pattern => isMatchingPattern(testString, pattern, requireExactStringMatch));
129+
}
130+
105131
/**
106132
* Given a string, escape characters which have meaning in the regex grammar, such that the result is safe to feed to
107133
* `new RegExp()`.

packages/utils/test/string.test.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isMatchingPattern, truncate } from '../src/string';
1+
import { isMatchingPattern, stringMatchesSomePattern, truncate } from '../src/string';
22

33
describe('truncate()', () => {
44
test('it works as expected', () => {
@@ -18,13 +18,31 @@ describe('truncate()', () => {
1818
});
1919

2020
describe('isMatchingPattern()', () => {
21-
test('match using string substring', () => {
21+
test('match using string substring if `requireExactStringMatch` not given', () => {
2222
expect(isMatchingPattern('foobar', 'foobar')).toEqual(true);
2323
expect(isMatchingPattern('foobar', 'foo')).toEqual(true);
2424
expect(isMatchingPattern('foobar', 'bar')).toEqual(true);
2525
expect(isMatchingPattern('foobar', 'nope')).toEqual(false);
2626
});
2727

28+
test('match using string substring if `requireExactStringMatch` is `false`', () => {
29+
expect(isMatchingPattern('foobar', 'foobar', false)).toEqual(true);
30+
expect(isMatchingPattern('foobar', 'foo', false)).toEqual(true);
31+
expect(isMatchingPattern('foobar', 'bar', false)).toEqual(true);
32+
expect(isMatchingPattern('foobar', 'nope', false)).toEqual(false);
33+
});
34+
35+
test('match using exact string match if `requireExactStringMatch` is `true`', () => {
36+
expect(isMatchingPattern('foobar', 'foobar', true)).toEqual(true);
37+
expect(isMatchingPattern('foobar', 'foo', true)).toEqual(false);
38+
expect(isMatchingPattern('foobar', 'nope', true)).toEqual(false);
39+
});
40+
41+
test('matches when `value` constains `pattern` but not vice-versa', () => {
42+
expect(isMatchingPattern('foobar', 'foo')).toEqual(true);
43+
expect(isMatchingPattern('foobar', 'foobarbaz')).toEqual(false);
44+
});
45+
2846
test('match using regexp test', () => {
2947
expect(isMatchingPattern('foobar', /^foo/)).toEqual(true);
3048
expect(isMatchingPattern('foobar', /foo/)).toEqual(true);
@@ -45,3 +63,48 @@ describe('isMatchingPattern()', () => {
4563
expect(isMatchingPattern([] as any, 'foo')).toEqual(false);
4664
});
4765
});
66+
67+
describe('stringMatchesSomePattern()', () => {
68+
test('match using string substring if `requireExactStringMatch` not given', () => {
69+
expect(stringMatchesSomePattern('foobar', ['foobar', 'nope'])).toEqual(true);
70+
expect(stringMatchesSomePattern('foobar', ['foo', 'nope'])).toEqual(true);
71+
expect(stringMatchesSomePattern('foobar', ['baz', 'nope'])).toEqual(false);
72+
});
73+
74+
test('match using string substring if `requireExactStringMatch` is `false`', () => {
75+
expect(stringMatchesSomePattern('foobar', ['foobar', 'nope'], false)).toEqual(true);
76+
expect(stringMatchesSomePattern('foobar', ['foo', 'nope'], false)).toEqual(true);
77+
expect(stringMatchesSomePattern('foobar', ['baz', 'nope'], false)).toEqual(false);
78+
});
79+
80+
test('match using exact string match if `requireExactStringMatch` is `true`', () => {
81+
expect(stringMatchesSomePattern('foobar', ['foobar', 'nope'], true)).toEqual(true);
82+
expect(stringMatchesSomePattern('foobar', ['foo', 'nope'], true)).toEqual(false);
83+
expect(stringMatchesSomePattern('foobar', ['baz', 'nope'], true)).toEqual(false);
84+
});
85+
86+
test('matches when `testString` constains a pattern but not vice-versa', () => {
87+
expect(stringMatchesSomePattern('foobar', ['foo', 'nope'])).toEqual(true);
88+
expect(stringMatchesSomePattern('foobar', ['foobarbaz', 'nope'])).toEqual(false);
89+
});
90+
91+
test('match using regexp test', () => {
92+
expect(stringMatchesSomePattern('foobar', [/^foo/, 'nope'])).toEqual(true);
93+
expect(stringMatchesSomePattern('foobar', [/foo/, 'nope'])).toEqual(true);
94+
expect(stringMatchesSomePattern('foobar', [/b.{1}r/, 'nope'])).toEqual(true);
95+
expect(stringMatchesSomePattern('foobar', [/^foo$/, 'nope'])).toEqual(false);
96+
});
97+
98+
test('should match empty pattern as true', () => {
99+
expect(stringMatchesSomePattern('foo', ['', 'nope'])).toEqual(true);
100+
expect(stringMatchesSomePattern('bar', ['', 'nope'])).toEqual(true);
101+
expect(stringMatchesSomePattern('', ['', 'nope'])).toEqual(true);
102+
});
103+
104+
test('should bail out with false when given non-string value', () => {
105+
expect(stringMatchesSomePattern(null as any, ['foo', 'nope'])).toEqual(false);
106+
expect(stringMatchesSomePattern(undefined as any, ['foo', 'nope'])).toEqual(false);
107+
expect(stringMatchesSomePattern({} as any, ['foo', 'nope'])).toEqual(false);
108+
expect(stringMatchesSomePattern([] as any, ['foo', 'nope'])).toEqual(false);
109+
});
110+
});

0 commit comments

Comments
 (0)