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

make SplunkPostDocLoadResourceInstrumentation aware of upstream context (#357) #398

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions integration-tests/otel-api-globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable header/header */
import { trace, context } from '@opentelemetry/api';

export { trace, context };
2 changes: 1 addition & 1 deletion integration-tests/tests/cdn/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ module.exports = {
}

const rumScriptFetchSpan = await browser.globals.findSpan(
(s) => s.name === 'resourceFetch'
(s) => s.name === 'resourceFetch' && s.tags['http.url'].includes('cdn.signalfx.com')
);
await browser.assert.ok(
!!rumScriptFetchSpan,
Expand Down
20 changes: 10 additions & 10 deletions integration-tests/tests/errors/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ module.exports = {
await browser.assert.strictEqual(tags['error.message'], ERROR_MESSAGE_MAP[browserName]);

const ERROR_STACK_MAP = {
safari: `global code@${url}:61:15`,
chrome: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:61:25`,
microsoftedge: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:61:25`,
firefox: `@${url}:61:7\n`,
'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:61:7)`,
safari: `global code@${url}:62:15`,
chrome: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`,
microsoftedge: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`,
firefox: `@${url}:62:7\n`,
'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:62:7)`,
};
await browser.assert.strictEqual(tags['error.stack'], ERROR_STACK_MAP[browserName]);
},
Expand Down Expand Up @@ -156,11 +156,11 @@ module.exports = {
await browser.assert.strictEqual(tags['error.message'], ERROR_MESSAGE_MAP[browserName]);

const ERROR_STACK_MAP = {
safari: `global code@${url}:61:15`,
microsoftedge: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:61:25`,
chrome: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:61:25`,
firefox: `@${url}:61:7\n`,
'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:61:7)`,
safari: `global code@${url}:62:15`,
microsoftedge: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`,
chrome: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`,
firefox: `@${url}:62:7\n`,
'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:62:7)`,
};
await browser.assert.strictEqual(tags['error.stack'], ERROR_STACK_MAP[browserName]);
},
Expand Down
51 changes: 51 additions & 0 deletions integration-tests/tests/resource-observer/resource-obs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,56 @@ module.exports = {
const afterLoadImage = imageSpans.find( span => span.tags['component'] === 'splunk-post-doc-load-resource');

await browser.assert.notEqual(docLoadImage.traceId, afterLoadImage.traceId);
},
'should propagate tracing context to created spans': async function(browser) {
if (isBrowser(browser, {
safari: true,
edge: true,
ie: true,
})) {
return;
}
await browser.url(browser.globals.getUrl('/resource-observer/resources-custom-context.ejs'));
await browser.globals.findSpan(span => span.name === 'guard-span');

const plImageRootSpan = await browser.globals.getReceivedSpans().find(
span => span.tags['http.url'] && span.tags['http.url'].endsWith('splunk-black.png')
&& span.tags['component'] === 'splunk-post-doc-load-resource'
);
const plImageParentSpan = await browser.globals.getReceivedSpans().find(
span => span.name === 'image-parent'
);
const plImageChildSpan = await browser.globals.getReceivedSpans().find(
span => span.tags['http.url'] && span.tags['http.url'].endsWith('splunk-black.svg')
&& span.tags['component'] === 'splunk-post-doc-load-resource'
);
await browser.assert.ok(plImageRootSpan);
await browser.assert.ok(plImageParentSpan);
await browser.assert.ok(plImageChildSpan);

await browser.assert.notEqual(plImageRootSpan.traceId, plImageParentSpan.traceId);
await browser.assert.not.ok(plImageRootSpan.parentId);
await browser.assert.equal(plImageParentSpan.traceId, plImageChildSpan.traceId);
await browser.assert.equal(plImageChildSpan.parentId, plImageParentSpan.id);

const plScriptRootSpan = await browser.globals.getReceivedSpans().find(
span => span.tags['http.url'] && span.tags['http.url'].endsWith('test.js')
&& span.tags['component'] === 'splunk-post-doc-load-resource'
);
const plScriptParentSpan = await browser.globals.getReceivedSpans().find(
span => span.name === 'script-parent'
);
const plScriptChildSpan = await browser.globals.getReceivedSpans().find(
span => span.tags['http.url'] && span.tags['http.url'].endsWith('test-alt.js')
&& span.tags['component'] === 'splunk-post-doc-load-resource'
);
await browser.assert.ok(plScriptRootSpan);
await browser.assert.ok(plScriptParentSpan);
await browser.assert.ok(plScriptChildSpan);

await browser.assert.notEqual(plScriptRootSpan.traceId, plScriptParentSpan.traceId);
await browser.assert.not.ok(plScriptRootSpan.parentId);
await browser.assert.equal(plScriptParentSpan.traceId, plScriptChildSpan.traceId);
await browser.assert.equal(plScriptChildSpan.parentId, plScriptParentSpan.id);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Resource observer test</title>
<%- renderAgent() %>
</head>
<body>
<h1>Resource observer test</h1>
<img src="/utils/devServer/assets/no-cache.png" alt="no-cache">

<script>
function loadScript(url) {
var p = new Promise(function(resolve, _) {
var s = document.createElement( 'script' );
s.setAttribute( 'src', url || '/utils/devServer/assets/test.js' );
s.onload = function() {
console.log('script resolved', performance.now());
resolve();
};
document.head.appendChild(s);
console.log('script load end', performance.now());
});
return p;
}

function loadImg(url) {
console.log('image load start', performance.now());
var p = new Promise(function(resolve, _) {
var img = document.createElement('img');
img.src = url || '/utils/devServer/assets/splunk-black.png';
img.onload = function() {
console.log('image resolved', performance.now());
resolve();
};
document.head.appendChild(img);
console.log('load img end ', performance.now())
});
return p;
}

function loadScriptCustomContext() {
var span = SplunkRum.provider.getTracer('default').startSpan('script-parent');
var context = OtelApiGlobals.context;
var trace = OtelApiGlobals.trace
return context.with(trace.setSpan(context.active(), span), function() {
return loadScript('/utils/devServer/assets/test-alt.js').finally(() => span.end());
});
}

function loadImageCustomContext() {
var span = SplunkRum.provider.getTracer('default').startSpan('image-parent');
var context = OtelApiGlobals.context;
var trace = OtelApiGlobals.trace;
return context.with(trace.setSpan(context.active(), span), function() {
return loadImg('/utils/devServer/assets/splunk-black.svg').finally(() => span.end());
});
}

window.testing = true;
window.addEventListener("load", function() {
console.log('test doc-load', performance.now())
Promise.all([loadImg(), loadScript(), loadImageCustomContext(), loadScriptCustomContext()]).then(function() {
console.log('test promise-all', performance.now())
SplunkRum.provider.getTracer('default').startSpan('guard-span').end();
window.testing = false;
})
})
</script>

</body>
</html>
27 changes: 27 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,31 @@ export default [
],
context: 'window',
},
{
input: 'integration-tests/otel-api-globals.ts',
output: {
file: 'dist/artifacts/otel-api-globals.js',
format: 'iife',
name: 'OtelApiGlobals',
sourcemap: true,
},
plugins: [
json(),
nodeResolvePlugin,
commonjs({
include: /node_modules/,
sourceMap: true,
transformMixedEsModules: true,
}),
typescript({ tsconfig: './tsconfig.base.json' }),
babelPlugin,
terser({
ecma: 5,
output: {
comments: false
}
}),
],
context: 'window',
}
];
11 changes: 10 additions & 1 deletion src/SplunkContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { getOriginalFunction, isFunction, wrapNatively } from './utils';
export interface ContextManagerConfig {
/** Enable async tracking of span parents */
async?: boolean;
onBeforeContextStart?: () => void;
onBeforeContextEnd?: () => void;
}

type EventListenerWithOrig = EventListener & {_orig?: EventListener};
Expand Down Expand Up @@ -621,13 +623,20 @@ export class SplunkContextManager implements ContextManager {
thisArg?: ThisParameterType<F>,
...args: A
): ReturnType<F> {
try {
this._config.onBeforeContextStart?.();
} catch (e) {
// ignore any exceptions thrown by context hooks
}
const previousContext = this._currentContext;
this._currentContext = context || ROOT_CONTEXT;

// Observe for location.hash changes (as it isn't a (re)configurable property))
const preLocationHash = location.hash;
try {
return fn.call(thisArg, ...args);
const result = fn.call(thisArg, ...args);
this._config.onBeforeContextEnd?.();
return result;
} finally {
this._currentContext = previousContext;
if (preLocationHash !== location.hash) {
Expand Down
59 changes: 51 additions & 8 deletions src/SplunkPostDocLoadResourceInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { VERSION } from './version';
import { hrTime, isUrlIgnored } from '@opentelemetry/core';
import { addSpanNetworkEvents } from '@opentelemetry/sdk-trace-web';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { context, Context, ROOT_CONTEXT } from '@opentelemetry/api';

export interface SplunkPostDocLoadResourceInstrumentationConfig extends InstrumentationConfig {
allowedInitiatorTypes?: string[];
Expand All @@ -30,8 +31,13 @@ export interface SplunkPostDocLoadResourceInstrumentationConfig extends Instrume

const MODULE_NAME = 'splunk-post-doc-load-resource';
const defaultAllowedInitiatorTypes = ['img', 'script']; //other, css, link

const nodeHasSrcAttribute = (node: Node): node is HTMLScriptElement | HTMLImageElement => (node instanceof HTMLScriptElement || node instanceof HTMLImageElement);

export class SplunkPostDocLoadResourceInstrumentation extends InstrumentationBase {
private observer: PerformanceObserver | undefined;
private performanceObserver: PerformanceObserver | undefined;
private headMutationObserver: MutationObserver | undefined;
private urlToContextMap: Record<string, Context>;
private config: SplunkPostDocLoadResourceInstrumentationConfig;

constructor(config: SplunkPostDocLoadResourceInstrumentationConfig = {}) {
Expand All @@ -42,26 +48,37 @@ export class SplunkPostDocLoadResourceInstrumentation extends InstrumentationBas
);
super(MODULE_NAME, VERSION, processedConfig);
this.config = processedConfig;
this.urlToContextMap = {};
}

init(): void {}

enable(): void {
if (window.PerformanceObserver) {
window.addEventListener('load', () => {
this._startObserver();
this._startPerformanceObserver();
});
}
if (window.MutationObserver) {
this._startHeadMutationObserver();
}
}

disable(): void {
if (this.observer) {
this.observer.disconnect();
if (this.performanceObserver) {
this.performanceObserver.disconnect();
}
if (this.headMutationObserver) {
this.headMutationObserver.disconnect();
}
}

private _startObserver() {
this.observer = new PerformanceObserver((list) => {
public onBeforeContextChange(): void {
this._processHeadMutationObserverRecords(this.headMutationObserver.takeRecords());
}

private _startPerformanceObserver() {
this.performanceObserver = new PerformanceObserver((list) => {
if (window.document.readyState === 'complete') {
list.getEntries().forEach(entry => {
// TODO: check how we can amend TS base typing to fix this
Expand All @@ -72,7 +89,31 @@ export class SplunkPostDocLoadResourceInstrumentation extends InstrumentationBas
}
});
//apparently safari 13.1 only supports entryTypes
this.observer.observe({ entryTypes: ['resource'] });
this.performanceObserver.observe({ entryTypes: ['resource'] });
}

private _startHeadMutationObserver() {
this.headMutationObserver = new MutationObserver(this._processHeadMutationObserverRecords.bind(this));
this.headMutationObserver.observe(document.head, { childList: true });
}

// for each added node that corresponds to a resource load, create an entry in `this.urlToContextMap`
// that associates its fully-qualified URL to the tracing context at the time that it was added
private _processHeadMutationObserverRecords(mutations: MutationRecord[]) {
if (context.active() === ROOT_CONTEXT) {
return;
}
mutations
.flatMap(mutation => Array.from(mutation.addedNodes || []))
.filter(nodeHasSrcAttribute)
.forEach((node) => {
const src = node.getAttribute('src');
if (!src) {
return;
}
const srcUrl = new URL(src, location.origin);
this.urlToContextMap[srcUrl.toString()] = context.active();
});
}

// TODO: discuss TS built-in types
Expand All @@ -81,13 +122,15 @@ export class SplunkPostDocLoadResourceInstrumentation extends InstrumentationBas
return;
}

const targetUrl = new URL(entry.name, location.origin);
const span = this.tracer.startSpan(
//TODO use @opentelemetry/instrumentation-document-load AttributeNames.RESOURCE_FETCH ?,
// AttributeNames not exported currently
'resourceFetch',
{
startTime: hrTime(entry.fetchStart),
}
},
this.urlToContextMap[targetUrl.toString()]
);
span.setAttribute('component', MODULE_NAME);
span.setAttribute(SemanticAttributes.HTTP_URL, entry.name);
Expand Down
12 changes: 9 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ let inited = false;
let _deregisterInstrumentations: () => void | undefined;
let _deinitSessionTracking: () => void | undefined;
let _errorInstrumentation: SplunkErrorInstrumentation | undefined;
let _postDocLoadInstrumentation: SplunkPostDocLoadResourceInstrumentation | undefined;
let eventTarget: InternalEventTarget | undefined;
export const SplunkRum: SplunkOtelWebType = {
DEFAULT_AUTO_INSTRUMENTED_EVENTS,
Expand Down Expand Up @@ -346,6 +347,9 @@ export const SplunkRum: SplunkOtelWebType = {
if (confKey === ERROR_INSTRUMENTATION_NAME && instrumentation instanceof SplunkErrorInstrumentation) {
_errorInstrumentation = instrumentation;
}
if (confKey === 'postload' && instrumentation instanceof SplunkPostDocLoadResourceInstrumentation) {
_postDocLoadInstrumentation = instrumentation;
}
return instrumentation;
}

Expand Down Expand Up @@ -380,9 +384,11 @@ export const SplunkRum: SplunkOtelWebType = {
});

provider.register({
contextManager: new SplunkContextManager(
processedOptions.context
)
contextManager: new SplunkContextManager({
...processedOptions.context,
onBeforeContextStart: () => _postDocLoadInstrumentation.onBeforeContextChange(),
onBeforeContextEnd: () => _postDocLoadInstrumentation.onBeforeContextChange(),
})
});

// After context manager registration so instrumentation event listeners are affected accordingly
Expand Down
Loading