diff --git a/README.md b/README.md index c8b0b94..d1fb9cf 100755 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Currently implemented features: * Automatic local context propagation using _Zone Context Manager_. [More details ↗](https://www.npmjs.com/package/@opentelemetry/context-zone) * Exporting collected spans to an OpenTelemetry collector. * Providing access to the OpenTelemtry Tracing-API for manual instrumentation. +* Adding global variables to spans during runtime via _addVarToSpans()_ +* Tracing of the whole transaction with the document load span as root span ### OpenTelemetry Plugins @@ -78,6 +80,8 @@ BOOMR.init({ instrument_document_load: { enabled: false, path: "", + recordTransaction: false, //If true, the transaction will be traced with the document load span as root span + exporterDelay: 20 // Delay to allow the exporter to export the transaction span before page unload }, instrument_user_interaction: { enabled: false, @@ -91,6 +95,14 @@ BOOMR.init({ instrument_document_load: true, instrument_user_interaction: true }, + // Additional instrumentation config, which will be applied to all plugins + global_instrumentation: { + // Include request parameter to spans and the corresponding beacons + requestParameter: { + enabled: false, + excludeKeysFromBeacons: [] //Keys, which should not be included in beacons, for instance due to cardinality concerns + } + }, exporter: { maxQueueSize: 100, maxExportBatchSize: 10, @@ -109,26 +121,28 @@ BOOMR.init({ ``` Available options are: -| Option | Description | Default value | -|---|---|---| -| `samplingRate` | Sampling rate to use when collecting spans. Value must be between `0` and `1`. | `1` | -| `corsUrls` | Array of CORS URLs to take into consideration when propagating trace information. By default, CORS URLs are excluded from the propagation. | `[]` | -| `collectorConfiguration` | Object that defines the OpenTelemetry collector configuration, like the URL to send spans to. See [CollectorExporterNodeConfigBase](https://www.npmjs.com/package/@opentelemetry/exporter-collector) interface for all options. | `undefined` | -| `consoleOnly` | If `true` spans will be logged on the console and not sent to the collector endpoint. | `false` | -| `plugins` | Object for enabling and disabling OpenTelemetry plugins. | | -| `plugins.instrument_fetch` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-fetch) for insturmentation of the fetch API. This will only be used in case the `fetch` API exists. | `true` | -| `plugins.instrument_xhr` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-xml-http-request) for insturmentation of the XMLHttpRequest API. | `true` | -| `plugins.instrument_document_load` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-document-load) for insturmentation of the document load (initial request). | `true` | -| `plugins.instrument_user_interaction` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction) for insturmentation of user interactions. | `true` | -| `exporter` | Object for configuring the span exporter. Only used if `consoleOnly` is not enabled. || -| `exporter.maxQueueSize` | The maximum queue size. After the size is reached spans are dropped. | `100` | -| `exporter.maxExportBatchSize` | The maximum batch size of every export. It must be smaller or equal to `maxQueueSize`. | `10` | -| `exporter.scheduledDelayMillis` | The interval between two consecutive exports. | `500` | -| `exporter.exportTimeoutMillis` | How long the export can run before it is cancelled. | `30000` | -| `commonAttributes` | An Object defining common span attributes which will be added to each recorded span. | `{}` | -| `serviceName` | A `string` or function which can be used to set the spans' service name. A function can be defined for dynamically providing the service name, e.g. based on Boomerang values. | `undefined` | -| `prototypeExporterPatch` | Patches the OpenTelemetry collector-span-exporter, so it is compatible with the Prototype framework. This is only necessary and should only be activated, when the Prototype framework is used. [For more information see the linked file](https://github.com/NovatecConsulting/boomerang-opentelemetry-plugin/blob/master/src/impl/patchCollectorPrototype.ts). | `false` | -| `propagationHeader` | Defines the format of the context propagation header. Available formats: `TRACE_CONTEXT`, `B3_SINGLE`, `B3_MULTI` | `TRACE_CONTEXT` | +| Option | Description | Default value | +|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---| +| `samplingRate` | Sampling rate to use when collecting spans. Value must be between `0` and `1`. | `1` | +| `corsUrls` | Array of CORS URLs to take into consideration when propagating trace information. By default, CORS URLs are excluded from the propagation. | `[]` | +| `collectorConfiguration` | Object that defines the OpenTelemetry collector configuration, like the URL to send spans to. See [CollectorExporterNodeConfigBase](https://www.npmjs.com/package/@opentelemetry/exporter-collector) interface for all options. | `undefined` | +| `consoleOnly` | If `true` spans will be logged on the console and not sent to the collector endpoint. | `false` | +| `plugins` | Object for enabling and disabling OpenTelemetry plugins. | | +| `plugins.instrument_fetch` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-fetch) for insturmentation of the fetch API. This will only be used in case the `fetch` API exists. | `true` | +| `plugins.instrument_xhr` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-xml-http-request) for insturmentation of the XMLHttpRequest API. | `true` | +| `plugins.instrument_document_load` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-document-load) for insturmentation of the document load (initial request). | `true` | +| `plugins.instrument_user_interaction` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction) for insturmentation of user interactions. | `true` | +| `global_instrumentation` | Object for configuring additional instrumentations, which will be applied to every OpenTelemetry plugin. || +| `global_instrumentation.requestParameter` | If enabled, existing request parameters will be added as attributes to spans and, if not excluded, will be added to the corresponding beacon as well. || +| `exporter` | Object for configuring the span exporter. Only used if `consoleOnly` is not enabled. || +| `exporter.maxQueueSize` | The maximum queue size. After the size is reached spans are dropped. | `100` | +| `exporter.maxExportBatchSize` | The maximum batch size of every export. It must be smaller or equal to `maxQueueSize`. | `10` | +| `exporter.scheduledDelayMillis` | The interval between two consecutive exports. | `500` | +| `exporter.exportTimeoutMillis` | How long the export can run before it is cancelled. | `30000` | +| `commonAttributes` | An Object defining common span attributes which will be added to each recorded span. | `{}` | +| `serviceName` | A `string` or function which can be used to set the spans' service name. A function can be defined for dynamically providing the service name, e.g. based on Boomerang values. | `undefined` | +| `prototypeExporterPatch` | Patches the OpenTelemetry collector-span-exporter, so it is compatible with the Prototype framework. This is only necessary and should only be activated, when the Prototype framework is used. [For more information see the linked file](https://github.com/NovatecConsulting/boomerang-opentelemetry-plugin/blob/master/src/impl/patchCollectorPrototype.ts). | `false` | +| `propagationHeader` | Defines the format of the context propagation header. Available formats: `TRACE_CONTEXT`, `B3_SINGLE`, `B3_MULTI` | `TRACE_CONTEXT` | ## Manual Instrumentation @@ -157,7 +171,21 @@ span.end(); The plugin also provides direct access to the OpenTelemetry API via the following function: `getOpenTelemetryApi()`. This returns the OpenTelemetry API and can be used for more advanced data collection. -### Asynchronous inclusion of Boomerang +## Transaction Recording + +If `plugins.instrument_document_load.recordTransaction` is set `true`, the document load span will be kept open during the whole transaction +and will be used as root span. +This transaction span will stay open until page unload or until the function +`startNewTransaction(spanName: string)` was called. This function closes the current transaction span and opens a new one with +the provided span name. + +Additionally, during the page load the document load span will check the `Server-Timing`-response-header for an existing trace context. +If existing, this trace context will be used to create the document load span. The trace context has to be in the W3C-format. +The trace context should be included in the `Server-Timing`-header like this: + +`traceparent; desc="00-f524a0cf2c5246077dd36b094d8e1132-b5fa4f189acedb66-01"` + +## Asynchronous inclusion of Boomerang Make sure to check that `window.BOOMR.plugins.OpenTelemetry` actually exists prior to using it in your code in case you load boomerang asynchronously. diff --git a/package.json b/package.json index c923791..1426bbf 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boomerang-opentelemetry-plugin", - "version": "0.25.0-7", + "version": "0.25.0-9", "description": "This is a Boomerang plugin for collecting spans using the OpenTelemetry framework and exporting them, e.g., to an OpenTelemetry collector.", "repository": "https://github.com/NovatecConsulting/boomerang-opentelemetry-plugin", "license": "Apache-2.0", diff --git a/src/impl/index.ts b/src/impl/index.ts index 9ee4c74..3a4aff0 100644 --- a/src/impl/index.ts +++ b/src/impl/index.ts @@ -1,4 +1,4 @@ -import api, { context, trace, Span } from '@opentelemetry/api'; +import api, { context, trace, Span, SpanOptions, Context } from '@opentelemetry/api'; import { AlwaysOnSampler, AlwaysOffSampler, @@ -24,16 +24,18 @@ import { import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3'; -import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; -import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; -import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; -import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction'; import { PluginProperties, ContextFunction, PropagationHeader } from '../types'; import { patchExporter, patchExporterClass } from './patchCollectorPrototype'; import { MultiSpanProcessor, CustomSpanProcessor } from './spanProcessing'; -import { DocumentLoadServerTimingInstrumentation, patchTracer } from './documentLoadInstrumentation'; -import { CustomIdGenerator } from './transactionIdGeneration'; -import { TransactionSpanManager } from './transactionSpanManager'; +import { + CustomDocumentLoadInstrumentation, + patchTracerForTransactions +} from './instrumentation/documentLoadInstrumentation'; +import { CustomIdGenerator } from './transaction/transactionIdGeneration'; +import { TransactionSpanManager } from './transaction/transactionSpanManager'; +import { CustomXMLHttpRequestInstrumentation } from './instrumentation/xmlHttpRequestInstrumentation'; +import { CustomFetchInstrumentation } from './instrumentation/fetchInstrumentation'; +import { CustomUserInteractionInstrumentation } from './instrumentation/userInteractionInstrumentation'; /** * TODOs: @@ -67,7 +69,7 @@ export default class OpenTelemetryTracingImpl { applyCustomAttributesOnSpan: null, // (span: Span, xhr: XMLHttpRequest) => { }, propagateTraceHeaderCorsUrls: [], ignoreUrls: [], - clearTimingResources: false, + clearTimingResources: false }, instrument_document_load: { enabled: false, @@ -80,6 +82,12 @@ export default class OpenTelemetryTracingImpl { path: "", }, }, + global_instrumentation: { + requestParameter: { + enabled: false, + excludeKeysFromBeacons: [] + } + }, exporter: { maxQueueSize: 100, maxExportBatchSize: 10, @@ -176,10 +184,8 @@ export default class OpenTelemetryTracingImpl { // store the webtracer this.traceProvider = providerWithZone; - // If recordTransaction is enabled, patch the Tracer to always use the transaction span as root span - // and initialize the transaction data storage + // If recordTransaction is enabled, initialize the transaction manager if(this.isTransactionRecordingEnabled()) { - patchTracer(); const delay = this.props.plugins_config?.instrument_document_load?.exporterDelay; TransactionSpanManager.initialize(true, this.customIdGenerator); @@ -255,15 +261,22 @@ export default class OpenTelemetryTracingImpl { */ private instrumentTracerClass = () => { const { commonAttributes, serviceName } = this.props; - // don't patch the function if no attributes are defined - if (Object.keys(commonAttributes).length <= 0) { + + let startSpanFunction: (name: string, options?: SpanOptions, context?: Context) => (Span); + + // If recordTransaction is enabled, patch the Tracer to always use the transaction span as root span + if(this.isTransactionRecordingEnabled()) + startSpanFunction = patchTracerForTransactions(); + else + startSpanFunction = Tracer.prototype.startSpan; + + // don't patch the function if no attributes are defined AND no serviceName is defined + if (!serviceName && Object.keys(commonAttributes).length <= 0) { return; } - const originalStartSpanFunction = Tracer.prototype.startSpan; - Tracer.prototype.startSpan = function () { - const span: Span = originalStartSpanFunction.apply(this, arguments); + const span: Span = startSpanFunction.apply(this, arguments); // add common attributes to each span if (commonAttributes) { @@ -302,46 +315,46 @@ export default class OpenTelemetryTracingImpl { }; private getInstrumentationPlugins = () => { - const { plugins, corsUrls, plugins_config } = this.props; + const { + plugins, + corsUrls, + plugins_config, + global_instrumentation + } = this.props; const instrumentations: any = []; // Instrumentation for the document on load (initial request) if (plugins_config?.instrument_document_load?.enabled !== false) { - if(this.isTransactionRecordingEnabled()) - instrumentations.push(new DocumentLoadServerTimingInstrumentation(plugins_config.instrument_document_load)); - else - instrumentations.push(new DocumentLoadInstrumentation(plugins_config.instrument_document_load)); + instrumentations.push(new CustomDocumentLoadInstrumentation(plugins_config.instrument_document_load, global_instrumentation)); } else if (plugins?.instrument_document_load !== false) { - instrumentations.push(new DocumentLoadInstrumentation()); + instrumentations.push(new CustomDocumentLoadInstrumentation({}, global_instrumentation)); } // Instrumentation for user interactions if (plugins_config?.instrument_user_interaction?.enabled !== false) { - instrumentations.push(new UserInteractionInstrumentation(plugins_config.instrument_user_interaction)); + instrumentations.push(new CustomUserInteractionInstrumentation(plugins_config.instrument_user_interaction, global_instrumentation)); } else if (plugins?.instrument_user_interaction !== false) { - instrumentations.push(new UserInteractionInstrumentation()); + instrumentations.push(new CustomUserInteractionInstrumentation({}, global_instrumentation)); } // XMLHttpRequest Instrumentation for web plugin if (plugins_config?.instrument_xhr?.enabled !== false) { - instrumentations.push(new XMLHttpRequestInstrumentation(plugins_config.instrument_xhr)); + instrumentations.push(new CustomXMLHttpRequestInstrumentation(plugins_config.instrument_xhr, global_instrumentation)); } else if (plugins?.instrument_xhr !== false) { instrumentations.push( - new XMLHttpRequestInstrumentation({ - propagateTraceHeaderCorsUrls: corsUrls - }) + new CustomXMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls: corsUrls }, global_instrumentation) ); } // Instrumentation for the fetch API if available const isFetchAPISupported = 'fetch' in window; if (isFetchAPISupported && plugins_config?.instrument_fetch?.enabled !== false) { - instrumentations.push(new FetchInstrumentation(plugins_config.instrument_fetch)); + instrumentations.push(new CustomFetchInstrumentation(plugins_config.instrument_fetch, global_instrumentation)); } else if (isFetchAPISupported && plugins?.instrument_fetch !== false) { - instrumentations.push(new FetchInstrumentation()); + instrumentations.push(new CustomFetchInstrumentation({}, global_instrumentation)); } return instrumentations; diff --git a/src/impl/documentLoadInstrumentation.ts b/src/impl/instrumentation/documentLoadInstrumentation.ts similarity index 53% rename from src/impl/documentLoadInstrumentation.ts rename to src/impl/instrumentation/documentLoadInstrumentation.ts index 8f0d701..eec8a95 100644 --- a/src/impl/documentLoadInstrumentation.ts +++ b/src/impl/instrumentation/documentLoadInstrumentation.ts @@ -2,26 +2,34 @@ import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; import * as api from '@opentelemetry/api'; -import { captureTraceParentFromPerformanceEntries } from './servertiming'; +import { captureTraceParentFromPerformanceEntries } from '../transaction/servertiming'; import { PerformanceEntries } from '@opentelemetry/sdk-trace-web'; import { Span, Tracer } from '@opentelemetry/sdk-trace-base'; - import { isTracingSuppressed } from '@opentelemetry/core/build/src/trace/suppress-tracing' import { sanitizeAttributes } from '@opentelemetry/core/build/src/common/attributes'; -import { TransactionSpanManager } from './transactionSpanManager'; - -export interface DocumentLoadServerTimingInstrumentationConfig extends InstrumentationConfig { - recordTransaction: boolean; - exporterDelay: number; +import { TransactionSpanManager } from '../transaction/transactionSpanManager'; +import { addUrlParams } from './urlParams'; +import { GlobalInstrumentationConfig, RequestParameterConfig } from '../../types'; +import { Context, SpanOptions } from '@opentelemetry/api'; + +export interface CustomDocumentLoadInstrumentationConfig extends InstrumentationConfig { + recordTransaction?: boolean; + exporterDelay?: number; } /** * Patch the Tracer class to use the transaction span as root span + * For any additional instrumentation of the startSpan() function, you have to use the + * new returned function + * + * OpenTelemetry version: 0.25.0 + * + * @return new startSpan() function */ -export function patchTracer() { - // Overwrite startSpan in Tracer class +export function patchTracerForTransactions(): (name: string, options?: SpanOptions, context?: Context) => (api.Span) { + // Overwrite startSpan() in Tracer class // Copy of the original startSpan()-function with additional logic inside the function to determine the parentContext - Tracer.prototype.startSpan = function ( + const overwrittenFunction = function ( name: string, options: api.SpanOptions = {}, context = api.context.active() @@ -32,6 +40,12 @@ export function patchTracer() { return api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT); } + /* + ####################################### + OVERWRITTEN LOGIC START + ####################################### + */ + let parentContext; //getParent(options, context); if(options.root) parentContext = undefined; else parentContext = api.trace.getSpanContext(context); @@ -49,6 +63,12 @@ export function patchTracer() { if(transactionSpanId) spanId = transactionSpanId; } + /* + ####################################### + OVERWRITTEN LOGIC END + ####################################### + */ + let traceId; let traceState; let parentSpanId; @@ -101,6 +121,9 @@ export function patchTracer() { span.setAttributes(Object.assign(attributes, samplingResult.attributes)); return span; } + + Tracer.prototype.startSpan = overwrittenFunction; + return overwrittenFunction; } type PerformanceEntriesWithServerTiming = PerformanceEntries & {serverTiming?: ReadonlyArray<({name: string, duration: number, description: string})>} @@ -109,42 +132,69 @@ type ExposedDocumentLoadSuper = { _endSpan(span: api.Span | undefined, performanceName: string, entries: PerformanceEntries): void; } -export class DocumentLoadServerTimingInstrumentation extends DocumentLoadInstrumentation { +export class CustomDocumentLoadInstrumentation extends DocumentLoadInstrumentation { readonly component: string = 'document-load-server-timing'; moduleName = this.component; - constructor(config: DocumentLoadServerTimingInstrumentationConfig) { + // Per default transaction should not be recorded + private recordTransaction = false; + + constructor(config: CustomDocumentLoadInstrumentationConfig = {}, globalInstrumentationConfig: GlobalInstrumentationConfig) { super(config); + const { requestParameter} = globalInstrumentationConfig; + + if(config.recordTransaction) + this.recordTransaction = config.recordTransaction; + + //Store original functions in variables const exposedSuper = this as any as ExposedDocumentLoadSuper; const _superStartSpan: ExposedDocumentLoadSuper['_startSpan'] = exposedSuper._startSpan.bind(this); const _superEndSpan: ExposedDocumentLoadSuper['_endSpan'] = exposedSuper._endSpan.bind(this); - exposedSuper._startSpan = (spanName, performanceName, entries, parentSpan) => { - if (!(entries as PerformanceEntriesWithServerTiming).serverTiming && performance.getEntriesByType) { - const navEntries = performance.getEntriesByType('navigation'); - // @ts-ignore - if (navEntries[0]?.serverTiming) { + if(this.recordTransaction) { + //Override function + exposedSuper._startSpan = (spanName, performanceName, entries, parentSpan) => { + if (!(entries as PerformanceEntriesWithServerTiming).serverTiming && performance.getEntriesByType) { + const navEntries = performance.getEntriesByType('navigation'); // @ts-ignore - (entries as PerformanceEntriesWithServerTiming).serverTiming = navEntries[0].serverTiming; + if (navEntries[0]?.serverTiming) { + // @ts-ignore + (entries as PerformanceEntriesWithServerTiming).serverTiming = navEntries[0].serverTiming; + } } + captureTraceParentFromPerformanceEntries(entries); + + const span = _superStartSpan(spanName, performanceName, entries, parentSpan); + const exposedSpan = span as any as Span; + if(exposedSpan.name == "documentLoad") TransactionSpanManager.setTransactionSpan(span); + + if(span && exposedSpan.name == "documentLoad" && requestParameter?.enabled) + addUrlParams(span, location.href, requestParameter.excludeKeysFromBeacons); + + return span; } - captureTraceParentFromPerformanceEntries(entries); - const span = _superStartSpan(spanName, performanceName, entries, parentSpan); - const exposedSpan = span as any as Span; - if(exposedSpan.name == "documentLoad") TransactionSpanManager.setTransactionSpan(span); + //Override function + exposedSuper._endSpan = (span, performanceName, entries) => { + const transactionSpan = TransactionSpanManager.getTransactionSpan(); + // Don't close transactionSpan + // transactionSpan will be closed through "beforeunload"-event + if(transactionSpan && transactionSpan == span) return; - return span; + return _superEndSpan(span, performanceName, entries); + }; } + else { + //Override function + exposedSuper._startSpan = (spanName, performanceName, entries, parentSpan) => { + const span = _superStartSpan(spanName, performanceName, entries, parentSpan); + const exposedSpan = span as any as Span; - exposedSuper._endSpan = (span, performanceName, entries) => { + if(span && exposedSpan.name == "documentLoad" && requestParameter?.enabled) + addUrlParams(span, location.href, requestParameter.excludeKeysFromBeacons); - const transactionSpan = TransactionSpanManager.getTransactionSpan(); - // Don't close transactionSpan - // transactionSpan will be closed through "beforeunload"-event - if(transactionSpan && transactionSpan == span) return; - - return _superEndSpan(span, performanceName, entries); - }; + return span; + } + } } } diff --git a/src/impl/instrumentation/fetchInstrumentation.ts b/src/impl/instrumentation/fetchInstrumentation.ts new file mode 100644 index 0000000..395ff26 --- /dev/null +++ b/src/impl/instrumentation/fetchInstrumentation.ts @@ -0,0 +1,34 @@ +import * as api from '@opentelemetry/api'; +import { addUrlParams } from './urlParams'; +import { FetchInstrumentation, FetchInstrumentationConfig } from '@opentelemetry/instrumentation-fetch'; +import { GlobalInstrumentationConfig, RequestParameterConfig } from '../../types'; + +type ExposedFetchSuper = { + _createSpan(url: string, options: Partial): api.Span | undefined; +} + +export class CustomFetchInstrumentation extends FetchInstrumentation { + + constructor(config: FetchInstrumentationConfig = {}, globalInstrumentationConfig: GlobalInstrumentationConfig) { + super(config); + const { requestParameter} = globalInstrumentationConfig; + + //Store original function in variable + const exposedSuper = this as any as ExposedFetchSuper; + const _superCreateSpan: ExposedFetchSuper['_createSpan'] = exposedSuper._createSpan.bind(this); + + //Override function + exposedSuper._createSpan = (url, options = {}) => { + const span = _superCreateSpan(url, options); + + if(span && requestParameter?.enabled) + addUrlParams(span, url, requestParameter.excludeKeysFromBeacons); + + return span; + } + } +} + + + + diff --git a/src/impl/instrumentation/urlParams.ts b/src/impl/instrumentation/urlParams.ts new file mode 100644 index 0000000..5ceeb2f --- /dev/null +++ b/src/impl/instrumentation/urlParams.ts @@ -0,0 +1,22 @@ +import { Span } from '@opentelemetry/api'; + +/** + * Add url parameters to spans and if not excluded also to the current beacon + * @param span current span + * @param url complete request url + * @param excludeKeys list of keys, which should not be written to beacons + */ +export function addUrlParams(span: Span, url: string, excludeKeys: string[] = []){ + const urlParams = url.split("?")[1]; + + if(urlParams) { + const keyValuePairs = urlParams.split("&"); + for(const keyValue of keyValuePairs) { + const [key, value] = keyValue.split("="); + span.setAttribute(key, value); + + // if excludeKey equals null OR key is not included, add var to beacon + if(!excludeKeys || !excludeKeys.includes(key)) window.BOOMR.addVar(key, value); + } + } +} diff --git a/src/impl/instrumentation/userInteractionInstrumentation.ts b/src/impl/instrumentation/userInteractionInstrumentation.ts new file mode 100644 index 0000000..82c6b8a --- /dev/null +++ b/src/impl/instrumentation/userInteractionInstrumentation.ts @@ -0,0 +1,36 @@ +import * as api from '@opentelemetry/api'; +import { addUrlParams } from './urlParams'; +import { FetchInstrumentation, FetchInstrumentationConfig } from '@opentelemetry/instrumentation-fetch'; +import { GlobalInstrumentationConfig, RequestParameterConfig } from '../../types'; +import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +type ExposedUserInteractionSuper = { + _createSpan(element: EventTarget | null | undefined, eventName: string, parentSpan?: api.Span | undefined): api.Span | undefined; +} + +export class CustomUserInteractionInstrumentation extends UserInteractionInstrumentation { + + constructor(config: InstrumentationConfig = {}, globalInstrumentationConfig: GlobalInstrumentationConfig) { + super(config); + const { requestParameter} = globalInstrumentationConfig; + + //Store original function in variable + const exposedSuper = this as any as ExposedUserInteractionSuper; + const _superCreateSpan: ExposedUserInteractionSuper['_createSpan'] = exposedSuper._createSpan.bind(this); + + //Override function + exposedSuper._createSpan = (element, eventName, parentSpan) => { + const span = _superCreateSpan(element, eventName, parentSpan); + + if(span && requestParameter?.enabled) + addUrlParams(span, location.href, requestParameter.excludeKeysFromBeacons); + + return span; + } + } +} + + + + diff --git a/src/impl/instrumentation/xmlHttpRequestInstrumentation.ts b/src/impl/instrumentation/xmlHttpRequestInstrumentation.ts new file mode 100644 index 0000000..dc030b7 --- /dev/null +++ b/src/impl/instrumentation/xmlHttpRequestInstrumentation.ts @@ -0,0 +1,37 @@ +import * as api from '@opentelemetry/api'; +import { + XMLHttpRequestInstrumentation, + XMLHttpRequestInstrumentationConfig +} from '@opentelemetry/instrumentation-xml-http-request'; +import { addUrlParams } from './urlParams'; +import { GlobalInstrumentationConfig, RequestParameterConfig } from '../../types'; + +type ExposedXHRSuper = { + _createSpan(xhr: XMLHttpRequest, url: string, method: string): api.Span | undefined; +} + +export class CustomXMLHttpRequestInstrumentation extends XMLHttpRequestInstrumentation { + + constructor(config: XMLHttpRequestInstrumentationConfig = {}, globalInstrumentationConfig: GlobalInstrumentationConfig) { + super(config); + const { requestParameter} = globalInstrumentationConfig; + + //Store original function in variable + const exposedSuper = this as any as ExposedXHRSuper; + const _superCreateSpan: ExposedXHRSuper['_createSpan'] = exposedSuper._createSpan.bind(this); + + //Override function + exposedSuper._createSpan = (xhr, url, method) => { + const span = _superCreateSpan(xhr, url, method); + + if(span && requestParameter?.enabled) + addUrlParams(span, url, requestParameter.excludeKeysFromBeacons); + + return span; + } + } +} + + + + diff --git a/src/impl/servertiming.ts b/src/impl/transaction/servertiming.ts similarity index 100% rename from src/impl/servertiming.ts rename to src/impl/transaction/servertiming.ts diff --git a/src/impl/transactionIdGeneration.ts b/src/impl/transaction/transactionIdGeneration.ts similarity index 100% rename from src/impl/transactionIdGeneration.ts rename to src/impl/transaction/transactionIdGeneration.ts diff --git a/src/impl/transactionSpanManager.ts b/src/impl/transaction/transactionSpanManager.ts similarity index 97% rename from src/impl/transactionSpanManager.ts rename to src/impl/transaction/transactionSpanManager.ts index 684ade1..110a9ef 100644 --- a/src/impl/transactionSpanManager.ts +++ b/src/impl/transaction/transactionSpanManager.ts @@ -1,5 +1,4 @@ import api, { Span } from '@opentelemetry/api'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; import { CustomIdGenerator } from './transactionIdGeneration'; /** diff --git a/src/types.d.ts b/src/types.d.ts index 1677b73..e1cb394 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,9 +1,9 @@ import { PropagateTraceHeaderCorsUrls } from '@opentelemetry/sdk-trace-web'; import { CollectorExporterNodeConfigBase } from '@opentelemetry/exporter-collector'; -import { FetchInstrumentationConfig } from "@opentelemetry/instrumentation-fetch"; -import { XMLHttpRequestInstrumentationConfig } from "@opentelemetry/instrumentation-xml-http-request"; -import { DocumentLoadServerTimingInstrumentationConfig } from './impl/documentLoadInstrumentation'; +import { CustomDocumentLoadInstrumentationConfig } from './impl/instrumentation/documentLoadInstrumentation'; import { InstrumentationConfig } from "@opentelemetry/instrumentation"; +import { FetchInstrumentationConfig } from '@opentelemetry/instrumentation-fetch'; +import { XMLHttpRequestInstrumentationConfig } from '@opentelemetry/instrumentation-xml-http-request'; export interface PluginProperties { samplingRate: number; @@ -12,6 +12,7 @@ export interface PluginProperties { consoleOnly: boolean; plugins: OTPluginProperties; plugins_config: OTPluginConfig; + global_instrumentation: GlobalInstrumentationConfig; exporter: OTExportProperties; commonAttributes: StringMap; prototypeExporterPatch: boolean; @@ -39,10 +40,19 @@ export interface OTPluginProperties { export interface OTPluginConfig { instrument_fetch: FetchInstrumentationConfig; instrument_xhr: XMLHttpRequestInstrumentationConfig; - instrument_document_load: DocumentLoadServerTimingInstrumentationConfig; + instrument_document_load: CustomDocumentLoadInstrumentationConfig; instrument_user_interaction: InstrumentationConfig; } +export interface GlobalInstrumentationConfig { + requestParameter: RequestParameterConfig; +} + +export interface RequestParameterConfig { + enabled?: boolean; + excludeKeysFromBeacons: string[]; +} + export interface OTExportProperties { // The maximum queue size. After the size is reached spans are dropped. maxQueueSize: number;