generated from honeycombio/.github
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ Browser attributes span processor (but also so much more) (#45)
<!-- Thank you for contributing to the project! 💜 Please see our [OSS process document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#) to get an idea of how we operate. --> ## Which problem is this PR solving? This PR adds the `BrowserAttributesSpanProcessor` which adds rich browser attributes to every span. Attributes like the page location and screen height and width might change during the lifecycle of a session so they are better captured through a Span Processor rather than on the Resource. - Closes #11 ## Short description of the changes - `BrowserAttributesSpanProcessor` class that adds attributes to each span - `CompositeSpanProcessor` class that combines multiple span processors together since the base SDK does not support multiple span processors - Span processor builder that configures the `BatchSpanProcessor` with the exporter as well as the `BrowserAttributesSpanProcessor` and an optional user provided span processor - Had to switch the exporter being configured through this span processor builder since the base SDK does not support configuring an exporter through the `traceExporter` option if a `spanProcessor` is provided. ## How to verify that this has the expected result - Run the test app and check for the additional browser attributes - The data should still be exported to Honeycomb --------- Co-authored-by: Jamie Danielson <[email protected]>
- Loading branch information
1 parent
507ee22
commit 920d29b
Showing
8 changed files
with
345 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Resource } from '@opentelemetry/resources'; | ||
|
||
export function configureBrowserAttributesResource(): Resource { | ||
return new Resource({ | ||
'user_agent.original': navigator.userAgent, | ||
//https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop | ||
'browser.mobile': navigator.userAgent.includes('Mobi'), | ||
'browser.touch_screen_enabled': navigator.maxTouchPoints > 0, | ||
'browser.language': navigator.language, | ||
'screen.width': window.screen.width, | ||
'screen.height': window.screen.height, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Span } from '@opentelemetry/api'; | ||
import { SpanProcessor } from '@opentelemetry/sdk-trace-base'; | ||
|
||
/** | ||
* A {@link SpanProcessor} that adds browser specific attributes to each span | ||
* that might change over the course of a session. | ||
* Static attributes (e.g. User Agent) are added to the Resource. | ||
*/ | ||
export class BrowserAttributesSpanProcessor implements SpanProcessor { | ||
constructor() {} | ||
|
||
onStart(span: Span) { | ||
const { href, pathname, search, hash, hostname } = window.location; | ||
|
||
span.setAttributes({ | ||
'browser.width': window.innerWidth, | ||
'browser.height': window.innerHeight, | ||
'page.hash': hash, | ||
'page.url': href, | ||
'page.route': pathname, | ||
'page.hostname': hostname, | ||
'page.search': search, | ||
}); | ||
} | ||
|
||
onEnd() {} | ||
|
||
forceFlush() { | ||
return Promise.resolve(); | ||
} | ||
|
||
shutdown() { | ||
return Promise.resolve(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { HoneycombOptions } from './types'; | ||
import { BrowserAttributesSpanProcessor } from './browser-attributes-span-processor'; | ||
import { | ||
BatchSpanProcessor, | ||
ReadableSpan, | ||
Span, | ||
SpanProcessor, | ||
} from '@opentelemetry/sdk-trace-base'; | ||
import { Context } from '@opentelemetry/api'; | ||
import { configureHoneycombHttpJsonTraceExporter } from './http-json-trace-exporter'; | ||
|
||
/** | ||
* Builds and returns Span Processor that combines the BatchSpanProcessor, BrowserSpanProcessor, | ||
* and optionally a user provided Span Processor. | ||
* @param options The {@link HoneycombOptions} | ||
* @returns a {@link CompositeSpanProcessor} | ||
*/ | ||
export const configureSpanProcessors = (options?: HoneycombOptions) => { | ||
const honeycombSpanProcessor = new CompositeSpanProcessor(); | ||
|
||
// We have to configure the exporter here becuase the way the base SDK is setup | ||
// does not allow having both a `spanProcessor` and `traceExporter` configured. | ||
honeycombSpanProcessor.addProcessor( | ||
new BatchSpanProcessor(configureHoneycombHttpJsonTraceExporter(options)), | ||
); | ||
|
||
// we always want to add the browser attrs span processor | ||
honeycombSpanProcessor.addProcessor(new BrowserAttributesSpanProcessor()); | ||
|
||
// if there is a user provided span processor, add it to the composite span processor | ||
if (options?.spanProcessor) { | ||
honeycombSpanProcessor.addProcessor(options?.spanProcessor); | ||
} | ||
|
||
return honeycombSpanProcessor; | ||
}; | ||
|
||
/** | ||
* A {@link SpanProcessor} that combines multiple span processors into a single | ||
* span processor that can be passed into the SDKs `spanProcessor` option. | ||
*/ | ||
export class CompositeSpanProcessor implements SpanProcessor { | ||
private spanProcessors: Array<SpanProcessor> = []; | ||
|
||
public addProcessor(processor: SpanProcessor) { | ||
this.spanProcessors.push(processor); | ||
} | ||
|
||
public getSpanProcessors() { | ||
return this.spanProcessors; | ||
} | ||
|
||
onStart(span: Span, parentContext: Context): void { | ||
this.spanProcessors.forEach((processor) => { | ||
processor.onStart(span, parentContext); | ||
}); | ||
} | ||
|
||
onEnd(span: ReadableSpan): void { | ||
this.spanProcessors.forEach((processor) => { | ||
processor.onEnd(span); | ||
}); | ||
} | ||
|
||
forceFlush(): Promise<void> { | ||
return Promise.all( | ||
this.spanProcessors.map((processor) => processor.forceFlush()), | ||
).then(() => {}); | ||
} | ||
|
||
shutdown(): Promise<void> { | ||
return Promise.all( | ||
this.spanProcessors.map((processor) => processor.forceFlush()), | ||
).then(() => {}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { configureBrowserAttributesResource } from '../src/browser-attributes-resource'; | ||
import { Resource } from '@opentelemetry/resources'; | ||
|
||
test('it should return a Resource', () => { | ||
const resource = configureBrowserAttributesResource(); | ||
expect(resource).toBeInstanceOf(Resource); | ||
}); | ||
|
||
test('it should have location attributes', () => { | ||
const resource = configureBrowserAttributesResource(); | ||
expect(resource.attributes).toEqual({ | ||
'browser.language': 'en-US', | ||
'browser.mobile': false, | ||
'browser.touch_screen_enabled': false, | ||
'screen.height': 0, | ||
'screen.width': 0, | ||
// user agent will be different locally and on CI, | ||
// we're really only testing to make sure it gets the value | ||
'user_agent.original': navigator.userAgent, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/** | ||
* @jest-environment-options {"url": "http://something-something.com/some-page?search_params=yes&hello=hi#the-hash"} | ||
*/ | ||
|
||
import { BrowserAttributesSpanProcessor } from '../src/browser-attributes-span-processor'; | ||
import { ROOT_CONTEXT, SpanKind, TraceFlags } from '@opentelemetry/api'; | ||
import { BasicTracerProvider, Span } from '@opentelemetry/sdk-trace-base'; | ||
|
||
describe('BrowserAttributesSpanProcessor', () => { | ||
const browserAttrsSpanProcessor = new BrowserAttributesSpanProcessor(); | ||
|
||
let span: Span; | ||
|
||
beforeEach(() => { | ||
span = new Span( | ||
new BasicTracerProvider().getTracer('browser-attrs-testing'), | ||
ROOT_CONTEXT, | ||
'A Very Important Browser Span!', | ||
{ | ||
traceId: '', | ||
spanId: '', | ||
traceFlags: TraceFlags.SAMPLED, | ||
}, | ||
SpanKind.CLIENT, | ||
); | ||
}); | ||
|
||
test('Span processor adds extra browser attributes', () => { | ||
browserAttrsSpanProcessor.onStart(span); | ||
|
||
expect(span.attributes).toEqual({ | ||
'browser.width': 1024, | ||
'browser.height': 768, | ||
'page.hash': '#the-hash', | ||
'page.hostname': 'something-something.com', | ||
'page.route': '/some-page', | ||
'page.search': '?search_params=yes&hello=hi', | ||
'page.url': | ||
'http://something-something.com/some-page?search_params=yes&hello=hi#the-hash', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/** | ||
* @jest-environment-options {"url": "http://something-something.com/some-page?search_params=yes&hello=hi#the-hash"} | ||
*/ | ||
import { | ||
CompositeSpanProcessor, | ||
configureSpanProcessors, | ||
} from '../src/span-processor-builder'; | ||
import { | ||
BasicTracerProvider, | ||
BatchSpanProcessor, | ||
Span, | ||
SpanProcessor, | ||
} from '@opentelemetry/sdk-trace-base'; | ||
import { ROOT_CONTEXT, SpanKind, TraceFlags } from '@opentelemetry/api'; | ||
|
||
class TestSpanProcessorOne implements SpanProcessor { | ||
onStart(span: Span): void { | ||
span.setAttributes({ | ||
'processor1.name': 'TestSpanProcessorOne', | ||
}); | ||
} | ||
|
||
onEnd(): void {} | ||
|
||
forceFlush() { | ||
return Promise.resolve(); | ||
} | ||
|
||
shutdown() { | ||
return Promise.resolve(); | ||
} | ||
} | ||
|
||
class TestSpanProcessorTwo implements SpanProcessor { | ||
onStart(span: Span): void { | ||
span.setAttributes({ | ||
'processor2.name': 'TestSpanProcessorTwo', | ||
}); | ||
} | ||
|
||
onEnd(): void {} | ||
|
||
forceFlush() { | ||
return Promise.resolve(); | ||
} | ||
|
||
shutdown() { | ||
return Promise.resolve(); | ||
} | ||
} | ||
|
||
describe('CompositeSpanProcessor', () => { | ||
const compositeSpanProcessor = new CompositeSpanProcessor(); | ||
|
||
let span: Span; | ||
|
||
beforeEach(() => { | ||
span = new Span( | ||
new BasicTracerProvider().getTracer('browser-attrs-testing'), | ||
ROOT_CONTEXT, | ||
'A Very Important Browser Span!', | ||
{ | ||
traceId: '', | ||
spanId: '', | ||
traceFlags: TraceFlags.SAMPLED, | ||
}, | ||
SpanKind.CLIENT, | ||
); | ||
}); | ||
|
||
test('Combines multiple span processors', () => { | ||
compositeSpanProcessor.addProcessor(new TestSpanProcessorOne()); | ||
compositeSpanProcessor.addProcessor(new TestSpanProcessorTwo()); | ||
|
||
compositeSpanProcessor.onStart(span, ROOT_CONTEXT); | ||
|
||
expect(span.attributes).toEqual({ | ||
'processor1.name': 'TestSpanProcessorOne', | ||
'processor2.name': 'TestSpanProcessorTwo', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('configureSpanProcessors', () => { | ||
let span: Span; | ||
|
||
beforeEach(() => { | ||
span = new Span( | ||
new BasicTracerProvider().getTracer('browser-attrs-testing'), | ||
ROOT_CONTEXT, | ||
'A Very Important Browser Span!', | ||
{ | ||
traceId: '', | ||
spanId: '', | ||
traceFlags: TraceFlags.SAMPLED, | ||
}, | ||
SpanKind.CLIENT, | ||
); | ||
}); | ||
test('Configures BatchSpanProcessor & BrowserAttributesSpanProcessor by default', () => { | ||
const honeycombSpanProcessors = configureSpanProcessors({}); | ||
expect(honeycombSpanProcessors.getSpanProcessors()).toHaveLength(2); | ||
expect(honeycombSpanProcessors.getSpanProcessors()[0]).toBeInstanceOf( | ||
BatchSpanProcessor, | ||
); | ||
|
||
honeycombSpanProcessors.onStart(span, ROOT_CONTEXT); | ||
expect(span.attributes).toEqual({ | ||
'browser.width': 1024, | ||
'browser.height': 768, | ||
'page.hash': '#the-hash', | ||
'page.hostname': 'something-something.com', | ||
'page.route': '/some-page', | ||
'page.search': '?search_params=yes&hello=hi', | ||
'page.url': | ||
'http://something-something.com/some-page?search_params=yes&hello=hi#the-hash', | ||
}); | ||
}); | ||
test('Configures additional user provided span processor', () => { | ||
const honeycombSpanProcessors = configureSpanProcessors({ | ||
spanProcessor: new TestSpanProcessorOne(), | ||
}); | ||
expect(honeycombSpanProcessors.getSpanProcessors()).toHaveLength(3); | ||
expect(honeycombSpanProcessors.getSpanProcessors()[0]).toBeInstanceOf( | ||
BatchSpanProcessor, | ||
); | ||
|
||
honeycombSpanProcessors.onStart(span, ROOT_CONTEXT); | ||
expect(span.attributes).toEqual({ | ||
'browser.width': 1024, | ||
'browser.height': 768, | ||
'page.hash': '#the-hash', | ||
'page.hostname': 'something-something.com', | ||
'page.route': '/some-page', | ||
'page.search': '?search_params=yes&hello=hi', | ||
'page.url': | ||
'http://something-something.com/some-page?search_params=yes&hello=hi#the-hash', | ||
'processor1.name': 'TestSpanProcessorOne', | ||
}); | ||
}); | ||
}); |