Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(instrumentation): Add allowUrls config option to web instrumentation #4938

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se

### :rocket: (Enhancement)

* feat(instrumentation): Add allowUrls config option to web instrumentation [#4938](https://github.com/open-telemetry/opentelemetry-js/pull/4938) @jairo-mendoza
* `XMLHttpRequestInstrumentation` and `FetchInstrumentation` now accept an `allowUrls` config option.
* `isUrlAllowed` function added to core. Both web instrumentation classes will depend on this function when using the new config option.

### :bug: (Bug Fix)

### :books: (Refine Doc)
Expand Down
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to experimental packages in this project will be documented

* feat(api-logs): Add delegating no-op logger provider [#4861](https://github.com/open-telemetry/opentelemetry-js/pull/4861) @hectorhdzg
* feat(instrumentation-http): Add support for client span semantic conventions 1.27 [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) @dyladan
* feat(instrumentation): Add allowUrls config option to web instrumentation [#4938](https://github.com/open-telemetry/opentelemetry-js/pull/4938) @jairo-mendoza

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig {
clearTimingResources?: boolean;
// urls which should include trace headers when origin doesn't match
propagateTraceHeaderCorsUrls?: web.PropagateTraceHeaderCorsUrls;
/**
* URLs that partially match any regex or exactly match strings in allowUrls
* will be traced.
*/
allowUrls?: Array<string | RegExp>;
/**
* URLs that partially match any regex in ignoreUrls will not be traced.
* In addition, URLs that are _exact matches_ of strings in ignoreUrls will
Expand Down Expand Up @@ -201,6 +206,10 @@ export class FetchInstrumentation extends InstrumentationBase<FetchInstrumentati
url: string,
options: Partial<Request | RequestInit> = {}
): api.Span | undefined {
if (!core.isUrlAllowed(url, this.getConfig().allowUrls)) {
this._diag.debug('ignoring span as url does not match an allowed url');
return;
}
if (core.isUrlIgnored(url, this.getConfig().ignoreUrls)) {
this._diag.debug('ignoring span as url matches ignored url');
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,40 @@ describe('fetch', () => {
});
});

describe('when url is allowed', () => {
beforeEach(async () => {
const propagateTraceHeaderCorsUrls = url;
await prepareData(url, {
propagateTraceHeaderCorsUrls,
allowUrls: [propagateTraceHeaderCorsUrls],
});
});
afterEach(() => {
clearData();
});
it('should create a span', () => {
assert.ok(exportSpy.args.length > 0, 'span should be exported');
});
});

describe('when url is NOT allowed', () => {
const otherUrl = 'http://localhost:8099/get';

beforeEach(async () => {
const propagateTraceHeaderCorsUrls = url;
await prepareData(url, {
propagateTraceHeaderCorsUrls,
allowUrls: [otherUrl],
});
});
afterEach(() => {
clearData();
});
it('should NOT create any span', () => {
assert.ok(exportSpy.args.length === 0, "span shouldn't be exported");
});
});

describe('when url is ignored', () => {
beforeEach(async () => {
const propagateTraceHeaderCorsUrls = url;
Expand All @@ -729,7 +763,11 @@ describe('fetch', () => {
clearData();
});
it('should NOT create any span', () => {
assert.strictEqual(exportSpy.args.length, 0, "span shouldn't b exported");
assert.strictEqual(
exportSpy.args.length,
0,
"span shouldn't be exported"
);
});
it('should pass request object as the first parameter to the original function (#2411)', () => {
const r = new Request(url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
InstrumentationConfig,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { hrTime, isUrlIgnored, otperformance } from '@opentelemetry/core';
import {
hrTime,
isUrlAllowed,
isUrlIgnored,
otperformance,
} from '@opentelemetry/core';
import {
SEMATTRS_HTTP_HOST,
SEMATTRS_HTTP_METHOD,
Expand Down Expand Up @@ -73,6 +78,11 @@ export interface XMLHttpRequestInstrumentationConfig
clearTimingResources?: boolean;
/** URLs which should include trace headers when origin doesn't match */
propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls;
/**
* URLs that partially match any regex or exactly match strings in allowUrls
* will be traced.
*/
allowUrls?: Array<string | RegExp>;
/**
* URLs that partially match any regex in ignoreUrls will not be traced.
* In addition, URLs that are _exact matches_ of strings in ignoreUrls will
Expand Down Expand Up @@ -331,6 +341,10 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
url: string,
method: string
): api.Span | undefined {
if (!isUrlAllowed(url, this.getConfig().allowUrls)) {
this._diag.debug('ignoring span as url does not match an allowed url');
return;
}
if (isUrlIgnored(url, this.getConfig().ignoreUrls)) {
this._diag.debug('ignoring span as url matches ignored url');
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,37 @@ describe('xhr', () => {
}
);

describe('when url is allowed', () => {
beforeEach(done => {
clearData();
const propagateTraceHeaderCorsUrls = url;
prepareData(done, url, {
propagateTraceHeaderCorsUrls,
allowUrls: [propagateTraceHeaderCorsUrls],
});
});

it('should create a span', () => {
assert.ok(exportSpy.called, 'span should be exported');
});
});

describe('when another url is allowed', () => {
const otherUrl = 'http://localhost:8099/get';
beforeEach(done => {
clearData();
const propagateTraceHeaderCorsUrls = url;
prepareData(done, url, {
propagateTraceHeaderCorsUrls,
allowUrls: [otherUrl],
});
});

it('should NOT create a span', () => {
assert.ok(exportSpy.notCalled, "span shouldn't be exported");
});
});

describe('when url is ignored', () => {
beforeEach(done => {
clearData();
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export {
export { merge } from './utils/merge';
export { TracesSamplerValues } from './utils/sampling';
export { TimeoutError, callWithTimeout } from './utils/timeout';
export { isUrlIgnored, urlMatches } from './utils/url';
export { isUrlAllowed, isUrlIgnored, urlMatches } from './utils/url';
export { isWrapped } from './utils/wrap';
export { BindOnceFuture } from './utils/callback';
export { VERSION } from './version';
Expand Down
20 changes: 20 additions & 0 deletions packages/opentelemetry-core/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,29 @@
if (typeof urlToMatch === 'string') {
return url === urlToMatch;
} else {
return !!url.match(urlToMatch);

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings with many repetitions of 'a'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of 'a'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of 'a'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of 'a'.
}
}
/**
* Check if {@param url} should be allowed when comparing against {@param allowedUrls}
* @param url
* @param allowedUrls
*/
export function isUrlAllowed(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit / fyi: as your introducing a new function to core, technically the xhr and fetch WILL require that they depend on this new version being released before they can consume this new function.

Generally, all versions are bumped together, but it might be worth calling out (somewhere)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Called out the new function in core in the main changelog, but I'm not 100% sure if it's the right spot for the call out

url: string,
allowedUrls?: Array<string | RegExp>
): boolean {
if (!allowedUrls) {
return true;
}

for (const allowedUrl of allowedUrls) {
if (urlMatches(url, allowedUrl)) {
return true;
}
}
return false;
}
/**
* Check if {@param url} should be ignored when comparing against {@param ignoredUrls}
* @param url
Expand Down