Skip to content

Commit e888059

Browse files
committed
chore: improve getElementXPath and adjust instrumentation to updated semconv PR
1 parent ba38e9b commit e888059

File tree

6 files changed

+141
-60
lines changed

6 files changed

+141
-60
lines changed

packages/instrumentation-user-action/src/instrumentation.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('UserActionInstrumentation', () => {
4848
};
4949

5050
const dispatchMouseDownEvent = (element: HTMLElement, button: number) => {
51-
const clickEvent = new MouseEvent('mousedown', {
51+
const clickEvent = new MouseEvent('click', {
5252
button,
5353
bubbles: true,
5454
clientX: 100,
@@ -67,12 +67,12 @@ describe('UserActionInstrumentation', () => {
6767

6868
const log = logs[0];
6969
expect(log?.severityNumber).toBe(SeverityNumber.INFO);
70-
expect(log?.eventName).toBe('browser.user_action');
71-
expect(log?.attributes['page.x']).toBe(100);
72-
expect(log?.attributes['page.y']).toBe(150);
73-
expect(log?.attributes['tag_name']).toBe('DIV');
74-
expect(log?.attributes['type']).toBe('mousedown.left');
75-
expect(log?.attributes['xpath']).toBe('//html/body/div');
70+
expect(log?.eventName).toBe('browser.user_action.click');
71+
expect(log?.attributes['browser.mouse_event.button']).toBe('left');
72+
expect(log?.attributes['browser.page.x']).toBe(100);
73+
expect(log?.attributes['browser.page.y']).toBe(150);
74+
expect(log?.attributes['browser.tag_name']).toBe('DIV');
75+
expect(log?.attributes['browser.xpath']).toBe('//html/body/div');
7676
});
7777

7878
it('should emit a log when the element triggers a mousedown event with the right and middle click', () => {
@@ -85,17 +85,21 @@ describe('UserActionInstrumentation', () => {
8585
expect(logs.length).toBe(2);
8686

8787
const middleClickLog = logs[0];
88-
expect(middleClickLog?.attributes['type']).toBe('mousedown.middle');
88+
expect(middleClickLog?.attributes['browser.mouse_event.button']).toBe(
89+
'middle',
90+
);
8991

9092
const rightClickLog = logs[1];
91-
expect(rightClickLog?.attributes['type']).toBe('mousedown.right');
93+
expect(rightClickLog?.attributes['browser.mouse_event.button']).toBe(
94+
'right',
95+
);
9296
});
9397

9498
it('should not emit a log when the event target is not an HTMLElement', () => {
9599
const textNode = document.createTextNode('Test Text Node');
96100
document.body.appendChild(textNode);
97101

98-
const clickEvent = new MouseEvent('mousedown', {
102+
const clickEvent = new MouseEvent('click', {
99103
button: 0,
100104
bubbles: true,
101105
clientX: 100,
@@ -128,7 +132,7 @@ describe('UserActionInstrumentation', () => {
128132
expect(logs.length).toBe(1);
129133

130134
const log = logs[0];
131-
expect(log?.attributes['tags']).toEqual({
135+
expect(log?.attributes['browser.element.attributes']).toEqual({
132136
'user-id': '12345',
133137
'session-id': 'abcde',
134138
});

packages/instrumentation-user-action/src/instrumentation.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ import { SeverityNumber } from '@opentelemetry/api-logs';
1818
import { InstrumentationBase } from '@opentelemetry/instrumentation';
1919
import { getElementXPath } from '@opentelemetry/web-utils';
2020
import {
21+
ATTR_MOUSE_EVENT_BUTTON,
2122
ATTR_PAGE_X,
2223
ATTR_PAGE_Y,
2324
ATTR_TAG_NAME,
2425
ATTR_TAGS,
25-
ATTR_TYPE,
2626
ATTR_XPATH,
27-
EVENT_NAME,
27+
CLICK_EVENT_NAME,
2828
} from './semconv';
2929
import type {
3030
AutoCapturedUserAction,
31-
UserActionEvent,
31+
MouseButton,
3232
UserActionInstrumentationConfig,
3333
} from './types';
3434

35-
const DEFAULT_AUTO_CAPTURED_ACTIONS: AutoCapturedUserAction[] = ['mousedown'];
35+
const DEFAULT_AUTO_CAPTURED_ACTIONS: AutoCapturedUserAction[] = ['click'];
3636
const OTEL_ELEMENT_ATTRIBUTE_PREFIX = 'data-otel-';
3737

3838
/**
@@ -47,16 +47,16 @@ export class UserActionInstrumentation extends InstrumentationBase<UserActionIns
4747
return [];
4848
}
4949

50-
private _getUserActionFromMouseEvent(event: MouseEvent): UserActionEvent {
50+
private _getMouseButtonFromMouseEvent(event: MouseEvent): MouseButton {
5151
switch (event.button) {
5252
case 0:
53-
return 'mousedown.left';
53+
return 'left';
5454
case 1:
55-
return 'mousedown.middle';
55+
return 'middle';
5656
case 2:
57-
return 'mousedown.right';
57+
return 'right';
5858
default:
59-
return 'mousedown.left';
59+
return 'left';
6060
}
6161
}
6262

@@ -71,7 +71,10 @@ export class UserActionInstrumentation extends InstrumentationBase<UserActionIns
7171
return;
7272
}
7373

74-
const xPath = getElementXPath(element, true);
74+
const xPath = getElementXPath(element, {
75+
useIdForTargetElement: true,
76+
useIdForAncestors: true,
77+
});
7578
const otelPrefixedAttributes: Record<string, string> = {};
7679

7780
// Grab all the attributes in the element that start with data-otel-*
@@ -85,13 +88,13 @@ export class UserActionInstrumentation extends InstrumentationBase<UserActionIns
8588

8689
this.logger.emit({
8790
severityNumber: SeverityNumber.INFO,
88-
eventName: EVENT_NAME,
91+
eventName: CLICK_EVENT_NAME,
8992
attributes: {
9093
[ATTR_PAGE_X]: event.pageX,
9194
[ATTR_PAGE_Y]: event.pageY,
9295
[ATTR_TAG_NAME]: element.tagName,
9396
[ATTR_TAGS]: otelPrefixedAttributes,
94-
[ATTR_TYPE]: this._getUserActionFromMouseEvent(event),
97+
[ATTR_MOUSE_EVENT_BUTTON]: this._getMouseButtonFromMouseEvent(event),
9598
[ATTR_XPATH]: xPath,
9699
},
97100
});
@@ -101,12 +104,12 @@ export class UserActionInstrumentation extends InstrumentationBase<UserActionIns
101104
const autoCapturedActions =
102105
this._config.autoCapturedActions ?? DEFAULT_AUTO_CAPTURED_ACTIONS;
103106

104-
if (autoCapturedActions.includes('mousedown')) {
105-
document.addEventListener('mousedown', this._clickHandler, true);
107+
if (autoCapturedActions.includes('click')) {
108+
document.addEventListener('click', this._clickHandler, true);
106109
}
107110
}
108111

109112
override disable(): void {
110-
document.removeEventListener('mousedown', this._clickHandler, true);
113+
document.removeEventListener('click', this._clickHandler, true);
111114
}
112115
}

packages/instrumentation-user-action/src/semconv.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,46 +20,46 @@
2020
* @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
2121
*/
2222

23-
export const EVENT_NAME = 'browser.user_action';
23+
export const CLICK_EVENT_NAME = 'browser.user_action.click';
2424

2525
/**
26-
* @example 10
26+
* @example left
2727
*
2828
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
2929
*/
30-
export const ATTR_PAGE_X = 'page.x';
30+
export const ATTR_MOUSE_EVENT_BUTTON = 'browser.mouse_event.button';
3131

3232
/**
3333
* @example 10
3434
*
3535
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
3636
*/
37-
export const ATTR_PAGE_Y = 'page.y';
37+
export const ATTR_PAGE_X = 'browser.page.x';
3838

3939
/**
40-
* @example "BUTTON"
40+
* @example 10
4141
*
4242
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
4343
*/
44-
export const ATTR_TAG_NAME = 'tag_name';
44+
export const ATTR_PAGE_Y = 'browser.page.y';
4545

4646
/**
47-
* @example ["id", "name"]
47+
* @example "BUTTON"
4848
*
4949
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
5050
*/
51-
export const ATTR_TAGS = 'tags';
51+
export const ATTR_TAG_NAME = 'browser.tag_name';
5252

5353
/**
54-
* @example "click.right"
54+
* @example {"id": "123", "name": "Name"}
5555
*
5656
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
5757
*/
58-
export const ATTR_TYPE = 'type';
58+
export const ATTR_TAGS = 'browser.element.attributes';
5959

6060
/**
6161
* @example "//*[@id='testBtn']"
6262
*
6363
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
6464
*/
65-
export const ATTR_XPATH = 'xpath';
65+
export const ATTR_XPATH = 'browser.xpath';

packages/instrumentation-user-action/src/types.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616

1717
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
1818

19-
export type AutoCapturedUserAction = 'mousedown';
19+
export type AutoCapturedUserAction = 'click';
2020

21-
export type UserActionEvent =
22-
| 'mousedown.left'
23-
| 'mousedown.middle'
24-
| 'mousedown.right';
21+
export type MouseButton = 'left' | 'middle' | 'right';
2522

2623
/**
2724
* UserActionInstrumentation Configuration

packages/web-utils/src/getElementXPath.test.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { describe, expect, it } from 'vitest';
17+
import { beforeEach, describe, expect, it } from 'vitest';
1818

1919
import { getElementXPath } from './getElementXPath';
2020

21-
describe(getElementXPath, () => {
21+
describe('getElementXPath', () => {
2222
const expectXPath = (xpath: string, node: Node) => {
2323
const result = document.evaluate(
2424
xpath,
@@ -31,24 +31,45 @@ describe(getElementXPath, () => {
3131
expect(result.singleNodeValue).toBe(node);
3232
};
3333

34+
beforeEach(() => {
35+
document.body.innerHTML = '';
36+
});
37+
3438
it('should return "/" for document node', () => {
3539
const xpath = getElementXPath(document);
3640

3741
expect(xpath).toBe('/');
3842
expectXPath(xpath, document);
3943
});
4044

41-
it('should return correct XPath for element with ID when optimized', () => {
45+
it('should return correct XPath for element with ID when useIdForTargetElement is true', () => {
4246
const div = document.createElement('div');
4347
div.id = 'test-id';
4448
document.body.appendChild(div);
4549

46-
const xpath = getElementXPath(div, true);
50+
const xpath = getElementXPath(div, {
51+
useIdForTargetElement: true,
52+
});
4753

4854
expect(xpath).toBe('//*[@id="test-id"]');
4955
expectXPath(xpath, div);
56+
});
57+
58+
it('should not use ID for element with ID when useIdForTargetElement is true but there are duplicated IDs', () => {
59+
const div = document.createElement('div');
60+
div.id = 'test-id';
61+
document.body.appendChild(div);
62+
63+
const duplicateDiv = document.createElement('div');
64+
duplicateDiv.id = 'test-id';
65+
document.body.appendChild(duplicateDiv);
5066

51-
document.body.removeChild(div);
67+
const xpath = getElementXPath(div, {
68+
useIdForTargetElement: true,
69+
});
70+
71+
expect(xpath).toBe('//html/body/div');
72+
expectXPath(xpath, div);
5273
});
5374

5475
it('should return correct XPath for nested elements', () => {
@@ -61,8 +82,21 @@ describe(getElementXPath, () => {
6182

6283
expect(xpath).toBe('//html/body/div/span');
6384
expectXPath(xpath, child);
85+
});
6486

65-
document.body.removeChild(parent);
87+
it('should return correct XPath using ID for nested elements', () => {
88+
const parent = document.createElement('div');
89+
parent.id = 'parent-id';
90+
const child = document.createElement('span');
91+
parent.appendChild(child);
92+
document.body.appendChild(parent);
93+
94+
const xpath = getElementXPath(child, {
95+
useIdForAncestors: true,
96+
});
97+
98+
expect(xpath).toBe('//html/body//*[@id="parent-id"]/span');
99+
expectXPath(xpath, child);
66100
});
67101

68102
it('should return correct XPath with index for sibling elements', () => {
@@ -77,7 +111,5 @@ describe(getElementXPath, () => {
77111

78112
expect(xpath).toBe('//html/body/div/span[2]');
79113
expectXPath(xpath, child2);
80-
81-
document.body.removeChild(parent);
82114
});
83115
});

0 commit comments

Comments
 (0)