Skip to content
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ dist
test-results
build-test-results
.turbo/
.claude/settings.local.json
.vscode
4 changes: 4 additions & 0 deletions packages/web-sdk/src/api-page/api/PageAPI/PageAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ describe('PageAPI', () => {
getCurrentRoute: sinon.stub().returns(mockRoute),
getCurrentPageId: sinon.stub().returns('test-page-id'),
clearCurrentRoute: sinon.stub(),
setPageLabel: sinon.stub(),
getPageLabel: sinon.stub(),
};
pageAPI.setGlobalPageManager(mockPageManager);
const pageManager = pageAPI.getPageManager();
Expand All @@ -55,6 +57,8 @@ describe('PageAPI', () => {
getCurrentRoute: sinon.stub().returns(mockRoute),
getCurrentPageId: sinon.stub().returns('test-page-id'),
clearCurrentRoute: sinon.stub(),
setPageLabel: sinon.stub(),
getPageLabel: sinon.stub(),
};
pageAPI.setGlobalPageManager(mockPageManager);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,11 @@ export class NoOpPageManager implements PageManager {
return '';
}

public setPageLabel(_label: string): void {}

public getPageLabel(): string | null {
return null;
}

public clearCurrentRoute(): void {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe('ProxyPageManager', () => {
getCurrentRoute: sinon.stub().returns(mockRoute),
getCurrentPageId: sinon.stub().returns('test-page-id'),
clearCurrentRoute: sinon.stub(),
setPageLabel: sinon.stub(),
getPageLabel: sinon.stub(),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ export class ProxyPageManager implements PageManager {
return this.getDelegate().getCurrentPageId();
}

public setPageLabel(label: string): void {
this.getDelegate().setPageLabel(label);
}

public getPageLabel(): string | null {
return this.getDelegate().getPageLabel();
}

public clearCurrentRoute(): void {
this.getDelegate().clearCurrentRoute();
}
Expand Down
6 changes: 6 additions & 0 deletions packages/web-sdk/src/api-page/manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export interface Route {
path: string;
// This is the URL of the route after replacing the URL params. i.e. /products/123
url: string;
// Optional label for the route, used as app.surface.label
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the examples given above. Maybe you could add something like:
"OLED Televisions - Electronics - BigStore.com"

label?: string;
}

export interface PageManager {
Expand All @@ -12,5 +14,9 @@ export interface PageManager {

getCurrentPageId: () => string | null;

setPageLabel: (label: string) => void;

getPageLabel: () => string | null;

clearCurrentRoute: () => void;
}
6 changes: 6 additions & 0 deletions packages/web-sdk/src/common/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { KEY_APP_SURFACE_LABEL } from '../constants/index.ts';

// These attributes are exposed in the public API for users to use in the attributes scrubber
export const attributes = {
pageLabel: KEY_APP_SURFACE_LABEL,
};
2 changes: 2 additions & 0 deletions packages/web-sdk/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { attributes } from './attributes.ts';
export type {
AttributeScrubber,
PathnameDocument,
TitleDocument,
URLDocument,
VisibilityStateDocument,
} from './types.ts';
5 changes: 5 additions & 0 deletions packages/web-sdk/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export interface PathnameDocument {
pathname: string;
}

// Useful for testing so that we can pass in a document-like object and change its title
export interface TitleDocument {
title: string;
}

export interface AttributeScrubber {
key: string;
scrub: (value: string) => string;
Expand Down
1 change: 1 addition & 0 deletions packages/web-sdk/src/constants/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const KEY_EMB_MAX_PENDING_SPANS_REACHED =
// to be consistent with mobile where we use 'app.surface.*' for screen names and ids
export const KEY_EMB_PAGE_PATH = 'app.surface.name';
export const KEY_EMB_PAGE_ID = 'app.surface.id';
export const KEY_APP_SURFACE_LABEL = 'app.surface.label';

export enum EMB_TYPES {
Session = 'ux.session',
Expand Down
1 change: 1 addition & 0 deletions packages/web-sdk/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
EMB_NAVIGATION_INSTRUMENTATIONS,
EMB_STATES,
EMB_TYPES,
KEY_APP_SURFACE_LABEL,
KEY_EMB_APP_INSTANCE_ID,
KEY_EMB_COLD_START,
KEY_EMB_ERROR_CODE,
Expand Down
1 change: 1 addition & 0 deletions packages/web-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { session } from './api-sessions/index.ts';
export type { ExtendedSpan } from './api-traces/index.ts';
export { trace } from './api-traces/index.ts';
export { user } from './api-users/index.ts';
export { attributes } from './common/index.ts';
export { getNavigationInstrumentation } from './instrumentations/index.ts';
export type { DynamicConfigManager, DynamicSDKConfig } from './sdk/index.ts';
export { initSDK } from './sdk/index.ts';
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import * as chai from 'chai';
import sinonChai from 'sinon-chai';
import { UUID_PATTERN } from '../../../tests/utils/constants.ts';
import type { Route } from '../../api-page/index.ts';
import type { TitleDocument } from '../../common/index.ts';
import { EmbracePageManager } from './EmbracePageManager.ts';

chai.use(sinonChai);
const { expect } = chai;

describe('EmbracePageManager', () => {
let pageManager: EmbracePageManager;
let mockDocument: TitleDocument;

beforeEach(() => {
pageManager = new EmbracePageManager();
mockDocument = { title: '' };
pageManager = new EmbracePageManager({ titleDocument: mockDocument });
});

it('should initialize with null values', () => {
Expand Down Expand Up @@ -72,4 +75,46 @@ describe('EmbracePageManager', () => {

expect(initialPageId).to.equal(secondPageId);
});

it('should set and get custom route label', () => {
pageManager.setPageLabel('my-custom-label');
expect(pageManager.getPageLabel()).to.equal('my-custom-label');
});

it('should fallback to document.title when custom label is not set', () => {
mockDocument.title = 'My Page Title';
expect(pageManager.getPageLabel()).to.equal('My Page Title');
});

it('should not fallback to document.title when fallback is disabled', () => {
const customPageManager = new EmbracePageManager({
useDocumentTitleAsPageLabel: false,
titleDocument: mockDocument,
});
mockDocument.title = 'My Page Title';
void expect(customPageManager.getPageLabel()).to.be.null;
});

it('should prefer custom label over document.title fallback', () => {
mockDocument.title = 'My Page Title';
pageManager.setPageLabel('custom-label');
expect(pageManager.getPageLabel()).to.equal('custom-label');
});

it('should set route label from route.label when setting route', () => {
const routeWithLabel: Route = {
path: '/products/:id',
url: '/products/123',
label: 'Products Page',
};
pageManager.setCurrentRoute(routeWithLabel);
expect(pageManager.getPageLabel()).to.equal('Products Page');
});

it('should clear custom label on routing', () => {
mockDocument.title = 'My Page Title';
pageManager.setPageLabel('custom-label');
pageManager.clearCurrentRoute();
expect(pageManager.getPageLabel()).to.equal('My Page Title');
});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
import type { PageManager, Route } from '../../api-page/index.ts';
import type { TitleDocument } from '../../common/index.ts';
import { generateUUID } from '../../utils/index.ts';
import type { EmbracePageManagerArgs } from './types.ts';

export class EmbracePageManager implements PageManager {
private _currentRoute: Route | null = null;
private _currentPageId: string | null = null;
private _pageLabel: string | null = null;
private readonly _titleDocument: TitleDocument | undefined;
private readonly _useDocumentTitleAsPageLabel: boolean;

public constructor({
useDocumentTitleAsPageLabel = true,
titleDocument = window.document,
}: EmbracePageManagerArgs = {}) {
this._useDocumentTitleAsPageLabel = useDocumentTitleAsPageLabel;
this._titleDocument = titleDocument;
}

public getCurrentPageId = (): string | null => this._currentPageId;

public getCurrentRoute = () => this._currentRoute;

public setPageLabel = (label: string): void => {
this._pageLabel = label;
};

public getPageLabel = (): string | null => {
return (
this._pageLabel ||
(!this._useDocumentTitleAsPageLabel || !this._titleDocument
? null
: this._titleDocument.title)
);
};

public setCurrentRoute = (route: Route) => {
if (!this._currentRoute || this._currentRoute.url !== route.url) {
this._currentPageId = generateUUID();
}

this._currentRoute = route;

if (route.label) {
this._pageLabel = route.label;
Copy link
Contributor

Choose a reason for hiding this comment

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

If the route changes but no label is set, it will reuse the previous label. Should this be cleared out instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, good catch

} else {
this._pageLabel = null;
}
};

public clearCurrentRoute = () => {
this._currentRoute = null;
this._currentPageId = null;
this._pageLabel = null;
};
}
3 changes: 3 additions & 0 deletions packages/web-sdk/src/managers/EmbracePageManager/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { DiagLogger } from '@opentelemetry/api';
import type { TitleDocument } from '../../common/index.ts';
import type { EMB_NAVIGATION_INSTRUMENTATIONS } from '../../constants/index.ts';

export interface EmbracePageManagerArgs {
diag?: DiagLogger;
shouldCleanupPathOptionsFromRouteName?: boolean;
useDocumentTitleAsPageLabel?: boolean;
titleDocument?: TitleDocument;
}

export interface SetCurrentRouteSpanOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import type { InMemoryLogRecordExporter } from '@opentelemetry/sdk-logs';
import * as chai from 'chai';
import { setupTestLogExporter } from '../../../tests/utils/index.ts';
import type { PageManager, Route } from '../../api-page/index.ts';
import { KEY_EMB_PAGE_ID, KEY_EMB_PAGE_PATH } from '../../constants/index.ts';
import {
KEY_APP_SURFACE_LABEL,
KEY_EMB_PAGE_ID,
KEY_EMB_PAGE_PATH,
} from '../../constants/index.ts';
import { EmbracePageManager } from '../../managers/index.ts';
import { PageLogRecordProcessor } from './PageLogRecordProcessor.ts';

Expand Down Expand Up @@ -49,6 +53,21 @@ describe('PageLogRecordProcessor', () => {
pageManager.getCurrentPageId(),
);
expect(log.attributes[KEY_EMB_PAGE_PATH]).to.equal('/products/:id');
void expect(log.attributes[KEY_APP_SURFACE_LABEL]).to.be.undefined;
});

it('should attach custom label when available', () => {
pageManager.setCurrentRoute(mockRoute);
pageManager.setPageLabel('CustomLabel');

logger.emit({
body: 'some log',
});

const finishedLogs = memoryExporter.getFinishedLogRecords();
const log = finishedLogs[finishedLogs.length - 1];

expect(log.attributes[KEY_APP_SURFACE_LABEL]).to.equal('CustomLabel');
});

it('should not override page attributes', () => {
Expand All @@ -70,6 +89,23 @@ describe('PageLogRecordProcessor', () => {
expect(log.attributes[KEY_EMB_PAGE_PATH]).to.equal('/custom/path');
});

it('should not override surface label attribute', () => {
pageManager.setCurrentRoute(mockRoute);
pageManager.setPageLabel('DefaultLabel');

logger.emit({
body: 'some log',
attributes: {
[KEY_APP_SURFACE_LABEL]: 'ExistingLabel',
},
});

const finishedLogs = memoryExporter.getFinishedLogRecords();
const log = finishedLogs[finishedLogs.length - 1];

expect(log.attributes[KEY_APP_SURFACE_LABEL]).to.equal('ExistingLabel');
});

it('should not attach surface name and id when route is null', () => {
pageManager.clearCurrentRoute();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { LogRecordProcessor, SdkLogRecord } from '@opentelemetry/sdk-logs';
import type { PageManager } from '../../api-page/index.ts';
import { KEY_EMB_PAGE_ID, KEY_EMB_PAGE_PATH } from '../../constants/index.ts';
import {
KEY_APP_SURFACE_LABEL,
KEY_EMB_PAGE_ID,
KEY_EMB_PAGE_PATH,
} from '../../constants/index.ts';
import type { PageLogRecordProcessorArgs } from './types.ts';

export class PageLogRecordProcessor implements LogRecordProcessor {
Expand All @@ -16,22 +20,25 @@ export class PageLogRecordProcessor implements LogRecordProcessor {
}

public onEmit(logRecord: SdkLogRecord): void {
// If the log already has page attributes, do not override them
if (
logRecord.attributes[KEY_EMB_PAGE_PATH] ||
logRecord.attributes[KEY_EMB_PAGE_ID]
!logRecord.attributes[KEY_EMB_PAGE_PATH] ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the logic changed here inadvertently, before it was skipping the setting if either were set, now it could override if one is set

Copy link
Contributor

Choose a reason for hiding this comment

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

If either is missing it overrides both

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, if one is missing I override both so make sure they are using the same route. This shouldn't happen in theory, we always set them at the same time but just to be safe. That's why before if either one was present we don't change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah yep that makes sense to me

!logRecord.attributes[KEY_EMB_PAGE_ID]
) {
// If the log already has page attributes, do not override them
return;
}
const currentRoute = this._pageManager.getCurrentRoute();

const currentRoute = this._pageManager.getCurrentRoute();
if (currentRoute) {
logRecord.setAttribute(KEY_EMB_PAGE_PATH, currentRoute.path);
logRecord.setAttribute(
KEY_EMB_PAGE_ID,
this._pageManager.getCurrentPageId(),
);
}
}

if (currentRoute) {
logRecord.setAttribute(KEY_EMB_PAGE_PATH, currentRoute.path);
logRecord.setAttribute(
KEY_EMB_PAGE_ID,
this._pageManager.getCurrentPageId(),
);
const appSurfaceLabel = this._pageManager.getPageLabel();
if (appSurfaceLabel && !logRecord.attributes[KEY_APP_SURFACE_LABEL]) {
logRecord.setAttribute(KEY_APP_SURFACE_LABEL, appSurfaceLabel);
}
}

Expand Down
Loading
Loading