opentelemetry-js/packages/opentelemetry-plugin-docume.../test/documentLoad.test.ts

583 lines
19 KiB
TypeScript

/*!
* Copyright 2019, OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Can't use Sinon Fake Time here as then cannot stub the performance getEntriesByType with desired metrics
*/
import {
context,
Logger,
PluginConfig,
propagation,
TimedEvent,
} from '@opentelemetry/api';
import {
ConsoleLogger,
HttpTraceContext,
TRACE_PARENT_HEADER,
} from '@opentelemetry/core';
import {
BasicTracerProvider,
ReadableSpan,
SimpleSpanProcessor,
SpanExporter,
} from '@opentelemetry/tracing';
import {
PerformanceTimingNames as PTN,
StackContextManager,
} from '@opentelemetry/web';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { ExportResult } from '../../opentelemetry-base/build/src';
import { DocumentLoad } from '../src';
export class DummyExporter implements SpanExporter {
export(
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void
) {}
shutdown() {}
}
const resources = [
{
name: 'http://localhost:8090/bundle.js',
entryType: 'resource',
startTime: 20.985000010114163,
duration: 90.94999998342246,
initiatorType: 'script',
nextHopProtocol: 'http/1.1',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 20.985000010114163,
domainLookupStart: 20.985000010114163,
domainLookupEnd: 20.985000010114163,
connectStart: 20.985000010114163,
connectEnd: 20.985000010114163,
secureConnectionStart: 20.985000010114163,
requestStart: 29.28999997675419,
responseStart: 31.88999998383224,
responseEnd: 111.93499999353662,
transferSize: 1446645,
encodedBodySize: 1446396,
decodedBodySize: 1446396,
serverTiming: [],
},
{
name: 'http://localhost:8090/sockjs-node/info?t=1572620894466',
entryType: 'resource',
startTime: 1998.5950000118464,
duration: 4.209999984595925,
initiatorType: 'xmlhttprequest',
nextHopProtocol: 'http/1.1',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 1998.5950000118464,
domainLookupStart: 1998.5950000118464,
domainLookupEnd: 1998.5950000118464,
connectStart: 1998.5950000118464,
connectEnd: 1998.5950000118464,
secureConnectionStart: 1998.5950000118464,
requestStart: 2001.7900000093505,
responseStart: 2002.3700000019744,
responseEnd: 2002.8049999964423,
transferSize: 368,
encodedBodySize: 79,
decodedBodySize: 79,
serverTiming: [],
},
];
const resourcesNoSecureConnectionStart = [
{
name: 'http://localhost:8090/bundle.js',
entryType: 'resource',
startTime: 20.985000010114163,
duration: 90.94999998342246,
initiatorType: 'script',
nextHopProtocol: 'http/1.1',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 20.985000010114163,
domainLookupStart: 20.985000010114163,
domainLookupEnd: 20.985000010114163,
connectStart: 20.985000010114163,
connectEnd: 20.985000010114163,
secureConnectionStart: 0,
requestStart: 29.28999997675419,
responseStart: 31.88999998383224,
responseEnd: 111.93499999353662,
transferSize: 1446645,
encodedBodySize: 1446396,
decodedBodySize: 1446396,
serverTiming: [],
},
];
const entries = {
name: 'http://localhost:8090/',
entryType: 'navigation',
startTime: 0,
duration: 374.0100000286475,
initiatorType: 'navigation',
nextHopProtocol: 'http/1.1',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 0.7999999215826392,
domainLookupStart: 0.7999999215826392,
domainLookupEnd: 0.7999999215826392,
connectStart: 0.7999999215826392,
connectEnd: 0.7999999215826393,
secureConnectionStart: 0.7999999215826392,
requestStart: 4.480000003241003,
responseStart: 5.729999975301325,
responseEnd: 6.154999951831996,
transferSize: 655,
encodedBodySize: 362,
decodedBodySize: 362,
serverTiming: [],
unloadEventStart: 12.63499993365258,
unloadEventEnd: 13.514999998733401,
domInteractive: 200.12499997392297,
domContentLoadedEventStart: 200.13999997172505,
domContentLoadedEventEnd: 201.6000000294298,
domComplete: 370.62499998137355,
loadEventStart: 370.64999993890524,
loadEventEnd: 374.0100000286475,
type: 'reload',
redirectCount: 0,
} as any;
const entriesFallback = {
navigationStart: 1571078170305,
unloadEventStart: 0,
unloadEventEnd: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 1571078170305,
domainLookupStart: 1571078170307,
domainLookupEnd: 1571078170308,
connectStart: 1571078170309,
connectEnd: 1571078170310,
secureConnectionStart: 1571078170310,
requestStart: 1571078170310,
responseStart: 1571078170313,
responseEnd: 1571078170330,
domLoading: 1571078170331,
domInteractive: 1571078170392,
domContentLoadedEventStart: 1571078170392,
domContentLoadedEventEnd: 1571078170392,
domComplete: 1571078170393,
loadEventStart: 1571078170393,
loadEventEnd: 1571078170394,
} as any;
function ensureNetworkEventsExists(events: TimedEvent[]) {
assert.strictEqual(events[0].name, PTN.FETCH_START);
assert.strictEqual(events[1].name, PTN.DOMAIN_LOOKUP_START);
assert.strictEqual(events[2].name, PTN.DOMAIN_LOOKUP_END);
assert.strictEqual(events[3].name, PTN.CONNECT_START);
assert.strictEqual(events[4].name, PTN.SECURE_CONNECTION_START);
assert.strictEqual(events[5].name, PTN.CONNECT_END);
assert.strictEqual(events[6].name, PTN.REQUEST_START);
assert.strictEqual(events[7].name, PTN.RESPONSE_START);
assert.strictEqual(events[8].name, PTN.RESPONSE_END);
}
describe('DocumentLoad Plugin', () => {
let plugin: DocumentLoad;
let moduleExports: any;
let provider: BasicTracerProvider;
let logger: Logger;
let config: PluginConfig;
let spanProcessor: SimpleSpanProcessor;
let dummyExporter: DummyExporter;
let contextManager: StackContextManager;
beforeEach(() => {
contextManager = new StackContextManager().enable();
context.setGlobalContextManager(contextManager);
Object.defineProperty(window.document, 'readyState', {
writable: true,
value: 'complete',
});
moduleExports = {};
provider = new BasicTracerProvider();
logger = new ConsoleLogger();
config = {};
plugin = new DocumentLoad();
dummyExporter = new DummyExporter();
spanProcessor = new SimpleSpanProcessor(dummyExporter);
provider.addSpanProcessor(spanProcessor);
});
afterEach(() => {
contextManager.disable();
Object.defineProperty(window.document, 'readyState', {
writable: true,
value: 'complete',
});
});
before(() => {
propagation.setGlobalPropagator(new HttpTraceContext());
});
describe('constructor', () => {
it('should construct an instance', () => {
plugin = new DocumentLoad();
assert.ok(plugin instanceof DocumentLoad);
});
});
describe('when document readyState is complete', () => {
let spyEntries: any;
beforeEach(() => {
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entries]);
spyEntries.withArgs('resource').returns([]);
});
afterEach(() => {
spyEntries.restore();
});
it('should start collecting the performance immediately', done => {
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
assert.strictEqual(window.document.readyState, 'complete');
assert.strictEqual(spyEntries.callCount, 2);
done();
});
});
});
describe('when document readyState is not complete', () => {
let spyEntries: any;
beforeEach(() => {
Object.defineProperty(window.document, 'readyState', {
writable: true,
value: 'loading',
});
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entries]);
spyEntries.withArgs('resource').returns([]);
});
afterEach(() => {
spyEntries.restore();
});
it('should collect performance after document load event', done => {
const spy = sinon.spy(window, 'addEventListener');
plugin.enable(moduleExports, provider, logger, config);
const args = spy.args[0];
const name = args[0];
assert.strictEqual(name, 'load');
assert.ok(spy.calledOnce);
assert.ok(spyEntries.callCount === 0);
window.dispatchEvent(
new CustomEvent('load', {
bubbles: true,
cancelable: false,
composed: true,
detail: {},
})
);
setTimeout(() => {
assert.strictEqual(spyEntries.callCount, 2);
done();
});
});
});
describe('when navigation entries types are available', () => {
let spyEntries: any;
beforeEach(() => {
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entries]);
spyEntries.withArgs('resource').returns([]);
});
afterEach(() => {
spyEntries.restore();
});
it('should export correct span with events', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const rootSpan = spyOnEnd.args[0][0][0] as ReadableSpan;
const fetchSpan = spyOnEnd.args[1][0][0] as ReadableSpan;
const rsEvents = rootSpan.events;
const fsEvents = fetchSpan.events;
assert.strictEqual(rootSpan.name, 'documentFetch');
assert.strictEqual(fetchSpan.name, 'documentLoad');
ensureNetworkEventsExists(rsEvents);
assert.strictEqual(fsEvents[0].name, PTN.FETCH_START);
assert.strictEqual(fsEvents[1].name, PTN.UNLOAD_EVENT_START);
assert.strictEqual(fsEvents[2].name, PTN.UNLOAD_EVENT_END);
assert.strictEqual(fsEvents[3].name, PTN.DOM_INTERACTIVE);
assert.strictEqual(
fsEvents[4].name,
PTN.DOM_CONTENT_LOADED_EVENT_START
);
assert.strictEqual(fsEvents[5].name, PTN.DOM_CONTENT_LOADED_EVENT_END);
assert.strictEqual(fsEvents[6].name, PTN.DOM_COMPLETE);
assert.strictEqual(fsEvents[7].name, PTN.LOAD_EVENT_START);
assert.strictEqual(fsEvents[8].name, PTN.LOAD_EVENT_END);
assert.strictEqual(rsEvents.length, 9);
assert.strictEqual(fsEvents.length, 9);
assert.strictEqual(spyOnEnd.callCount, 2);
done();
});
});
describe('AND window has information about server root span', () => {
let spyGetElementsByTagName: any;
beforeEach(() => {
const element = {
content: '00-ab42124a3c573678d4d8b21ba52df3bf-d21f7bc17caa5aba-01',
getAttribute: (value: string) => {
if (value === 'name') {
return TRACE_PARENT_HEADER;
}
return undefined;
},
};
spyGetElementsByTagName = sinon.stub(
window.document,
'getElementsByTagName'
);
spyGetElementsByTagName.withArgs('meta').returns([element]);
});
afterEach(() => {
spyGetElementsByTagName.restore();
});
it('should create a root span with server context traceId', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const rootSpan = spyOnEnd.args[0][0][0] as ReadableSpan;
const fetchSpan = spyOnEnd.args[1][0][0] as ReadableSpan;
assert.strictEqual(rootSpan.name, 'documentFetch');
assert.strictEqual(fetchSpan.name, 'documentLoad');
assert.strictEqual(
rootSpan.spanContext.traceId,
'ab42124a3c573678d4d8b21ba52df3bf'
);
assert.strictEqual(
fetchSpan.spanContext.traceId,
'ab42124a3c573678d4d8b21ba52df3bf'
);
assert.strictEqual(spyOnEnd.callCount, 2);
done();
}, 1);
});
});
});
describe('when resource entries are available', () => {
let spyEntries: any;
beforeEach(() => {
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entries]);
spyEntries.withArgs('resource').returns(resources);
});
afterEach(() => {
spyEntries.restore();
});
it('should create span for each of the resource', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const spanResource1 = spyOnEnd.args[1][0][0] as ReadableSpan;
const spanResource2 = spyOnEnd.args[2][0][0] as ReadableSpan;
const srEvents1 = spanResource1.events;
const srEvents2 = spanResource2.events;
assert.strictEqual(
spanResource1.name,
'http://localhost:8090/bundle.js'
);
assert.strictEqual(
spanResource2.name,
'http://localhost:8090/sockjs-node/info?t=1572620894466'
);
ensureNetworkEventsExists(srEvents1);
ensureNetworkEventsExists(srEvents2);
assert.strictEqual(spyOnEnd.callCount, 4);
done();
});
});
});
describe('when resource entries are available AND secureConnectionStart is 0', () => {
let spyEntries: any;
beforeEach(() => {
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entries]);
spyEntries.withArgs('resource').returns(resourcesNoSecureConnectionStart);
});
afterEach(() => {
spyEntries.restore();
});
it('should create span for each of the resource', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const spanResource1 = spyOnEnd.args[1][0][0] as ReadableSpan;
const srEvents1 = spanResource1.events;
assert.strictEqual(
spanResource1.name,
'http://localhost:8090/bundle.js'
);
assert.strictEqual(srEvents1[0].name, PTN.FETCH_START);
assert.strictEqual(srEvents1[1].name, PTN.DOMAIN_LOOKUP_START);
assert.strictEqual(srEvents1[2].name, PTN.DOMAIN_LOOKUP_END);
assert.strictEqual(srEvents1[3].name, PTN.CONNECT_START);
assert.strictEqual(srEvents1[4].name, PTN.CONNECT_END);
assert.strictEqual(srEvents1[5].name, PTN.REQUEST_START);
assert.strictEqual(srEvents1[6].name, PTN.RESPONSE_START);
assert.strictEqual(srEvents1[7].name, PTN.RESPONSE_END);
assert.strictEqual(spyOnEnd.callCount, 3);
done();
});
});
});
describe('when navigation entries types are available and property "loadEventEnd" is missing', () => {
let spyEntries: any;
beforeEach(() => {
const entriesWithoutLoadEventEnd = Object.assign({}, entries);
delete entriesWithoutLoadEventEnd.loadEventEnd;
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([entriesWithoutLoadEventEnd]);
spyEntries.withArgs('resource').returns([]);
});
afterEach(() => {
spyEntries.restore();
});
it('should still export rootSpan and fetchSpan', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const rootSpan = spyOnEnd.args[0][0][0] as ReadableSpan;
const fetchSpan = spyOnEnd.args[1][0][0] as ReadableSpan;
assert.strictEqual(rootSpan.name, 'documentFetch');
assert.strictEqual(fetchSpan.name, 'documentLoad');
assert.strictEqual(spyOnEnd.callCount, 2);
done();
});
});
});
describe('when navigation entries types are NOT available then fallback to "performance.timing"', () => {
let spyEntries: any;
beforeEach(() => {
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([]);
spyEntries.withArgs('resource').returns([]);
Object.defineProperty(window.performance, 'timing', {
writable: true,
value: entriesFallback,
});
});
afterEach(() => {
spyEntries.restore();
});
it('should export correct span with events', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
const rootSpan = spyOnEnd.args[0][0][0] as ReadableSpan;
const fetchSpan = spyOnEnd.args[1][0][0] as ReadableSpan;
const rsEvents = rootSpan.events;
const fsEvents = fetchSpan.events;
assert.strictEqual(rootSpan.name, 'documentFetch');
assert.strictEqual(fetchSpan.name, 'documentLoad');
ensureNetworkEventsExists(rsEvents);
assert.strictEqual(fsEvents[0].name, PTN.FETCH_START);
assert.strictEqual(fsEvents[1].name, PTN.DOM_INTERACTIVE);
assert.strictEqual(
fsEvents[2].name,
PTN.DOM_CONTENT_LOADED_EVENT_START
);
assert.strictEqual(fsEvents[3].name, PTN.DOM_CONTENT_LOADED_EVENT_END);
assert.strictEqual(fsEvents[4].name, PTN.DOM_COMPLETE);
assert.strictEqual(fsEvents[5].name, PTN.LOAD_EVENT_START);
assert.strictEqual(fsEvents[6].name, PTN.LOAD_EVENT_END);
assert.strictEqual(rsEvents.length, 9);
assert.strictEqual(fsEvents.length, 7);
assert.strictEqual(spyOnEnd.callCount, 2);
done();
});
});
});
describe('when navigation entries types and "performance.timing" are NOT available', () => {
let spyEntries: any;
beforeEach(() => {
Object.defineProperty(window.performance, 'timing', {
writable: true,
value: undefined,
});
spyEntries = sinon.stub(window.performance, 'getEntriesByType');
spyEntries.withArgs('navigation').returns([]);
spyEntries.withArgs('resource').returns([]);
});
afterEach(() => {
spyEntries.restore();
});
it('should not create any span', done => {
const spyOnEnd = sinon.spy(dummyExporter, 'export');
plugin.enable(moduleExports, provider, logger, config);
setTimeout(() => {
assert.ok(spyOnEnd.callCount === 0);
done();
});
});
});
});