Skip to content
Open
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
15 changes: 14 additions & 1 deletion packages/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Initialization settings:
| `beforeSend` | function(event) => event \| false \| void | optional | Filter data before sending. Return modified event, `false` to drop the event. |
| `issues` | IssuesOptions object | optional | Issues config. See [Issues configuration](#issues-configuration). |

Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition.
Other available [initial settings](src/types/hawk-initial-settings.ts) are described at the type definition.

## Manual sending

Expand Down Expand Up @@ -353,6 +353,19 @@ const hawk = new HawkCatcher({
});
```

## Yandex Metrika

If [Yandex Metrika](https://yandex.ru/support/metrica/) with Webvisor is installed on your site, Hawk automatically attaches visitor ClientIDs to every event. In Hawk Garage you can open the user's Webvisor session from the event details.

No additional Hawk configuration is required — initialize Hawk as usual and ensure Metrika counters are loaded on the page.

Hawk inspects up to 10 Metrika counters from the initialization queue and includes only those initialized with `webvisor: true`. For each matched counter, Hawk requests the visitor ClientID via the public [`getClientID`](https://yandex.ru/support/metrica/ru/objects/get-client-id) API and sends it in the event addon:

Requirements:

- Yandex Metrika script must be present on the page (`window.ym`)
- Counter must be initialized with Webvisor enabled (`webvisor: true` in init options)

## Source maps consuming

If your bundle is minified, it is useful to pass source-map files to the Hawk. After that you will see beautiful
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hawk.so/browser",
"version": "3.3.6",
"version": "3.4.0",
"description": "JavaScript Browser errors tracking for Hawk.so",
"files": [
"dist"
Expand Down
208 changes: 208 additions & 0 deletions packages/browser/src/addons/yandex-metrika-addon-message-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import type { MessageProcessor, ProcessingPayload } from '@hawk.so/core';

/**
* Addon key used to attach Yandex Metrika counters ClientIDs.
*/
export const YANDEX_METRIKA_ADDON_KEY = 'yandexMetrika';

/**
* Maximum number of Yandex Metrika initialization queue entries to inspect.
*/
const MAX_YANDEX_METRIKA_COUNTERS = 10;

/**
* Yandex Metrika global function used to call public Metrika methods.
*/
interface YandexMetrikaFunction {
(
counterId: number,
method: 'getClientID',
callback: (clientId: unknown) => void
): void;
a?: ArrayLike<ArrayLike<unknown>>;
}

/**
* Browser window with optional Yandex Metrika global function.
*/
type WindowWithYandexMetrika = Window & {
ym?: YandexMetrikaFunction;
};

/**
* Yandex Metrika counter ID paired with its visitor ClientID.
*/
interface YandexMetrikaCounterClientId {
/**
* Yandex Metrika counter ID.
*/
counterId: number;

/**
* Yandex Metrika visitor ClientID.
*/
clientId: string;
}

/**
* Reads up to ten Yandex Metrika counter IDs, requests their ClientIDs once
* during initialization, and attaches available counters ClientIDs to subsequent events.
*
* Important: `window.ym.a[index][0]` relies on the Metrika initialization queue
* and is not a public API contract. This is acceptable for the MVP, but the SDK
* should accept counter IDs explicitly in the future.
*
* @see https://yandex.ru/support/metrica/ru/objects/get-client-id
*/
export class YandexMetrikaAddonMessageProcessor implements MessageProcessor<'errors/javascript'> {
/**
* Cached Yandex Metrika counters ClientIDs keyed by their one-based queue position.
*/
private countersClientIds: Record<number, YandexMetrikaCounterClientId> = {};

/**
* Reads up to ten initialized counters and requests their ClientIDs.
*/
constructor() {
const ym = this.getYandexMetrika();

if (!ym) {
return;
}

this.collectCountersClientIds(ym);
}

/**
* Attaches cached Yandex Metrika counters ClientIDs when they are available.
*
* @param payload - event message payload to enrich
* @returns {ProcessingPayload<'errors/javascript'>} enriched or original payload
*/
public apply(
payload: ProcessingPayload<'errors/javascript'>
): ProcessingPayload<'errors/javascript'> {
if (Object.keys(this.countersClientIds).length > 0) {
(payload.addons as Record<string, unknown>)[YANDEX_METRIKA_ADDON_KEY] = {
...this.countersClientIds,
};
}

return payload;
}

/**
* Returns Yandex Metrika global function when it is installed on the page.
*/
private getYandexMetrika(): YandexMetrikaFunction | undefined {
const ym = (window as WindowWithYandexMetrika).ym;

return typeof ym === 'function' ? ym : undefined;
}

/**
* Reads initialized Yandex Metrika counters from the queue and requests ClientIDs.
*
* @param ym - Yandex Metrika global function.
*/
private collectCountersClientIds(ym: YandexMetrikaFunction): void {
for (let queueIndex = 0; queueIndex < MAX_YANDEX_METRIKA_COUNTERS; queueIndex++) {
this.collectCounterClientId(ym, queueIndex);
}
}

/**
* Requests ClientID for one valid Yandex Metrika counter queue entry.
*
* @param ym - Yandex Metrika global function.
* @param queueIndex - Zero-based Metrika initialization queue entry index.
*/
private collectCounterClientId(ym: YandexMetrikaFunction, queueIndex: number): void {
const counterId = this.getCounterIdFromQueue(ym, queueIndex);

if (counterId === undefined) {
return;
}

this.requestClientId(ym, queueIndex, counterId);
}

/**
* Returns valid counter ID from the Yandex Metrika initialization queue.
*
* @param ym - Yandex Metrika global function.
* @param queueIndex - Zero-based Metrika initialization queue entry index.
*/
private getCounterIdFromQueue(
ym: YandexMetrikaFunction,
queueIndex: number
): number | undefined {
const queueEntry = ym.a?.[queueIndex];
const counterId = this.parseCounterId(queueEntry?.[0]);
const options = queueEntry?.[2] as { webvisor?: unknown } | undefined;

if (counterId === undefined || options?.webvisor !== true) {
return undefined;
}

return counterId;
}

/**
* Converts raw counter ID from the Metrika queue to a positive integer.
*
* @param rawCounterId - Counter ID read from the Metrika queue.
*/
private parseCounterId(rawCounterId: unknown): number | undefined {
const counterId = typeof rawCounterId === 'number' || typeof rawCounterId === 'string'
? Number(rawCounterId)
: NaN;

if (!Number.isSafeInteger(counterId) || counterId <= 0) {
return undefined;
}

return counterId;
}

/**
* Requests ClientID from Yandex Metrika and caches it when it is available.
*
* @param ym - Yandex Metrika global function.
* @param queueIndex - Zero-based Metrika initialization queue entry index.
* @param counterId - Yandex Metrika counter ID.
*/
private requestClientId(
ym: YandexMetrikaFunction,
queueIndex: number,
counterId: number
): void {
try {
ym(counterId, 'getClientID', (clientId) => {
this.saveCounterClientId(queueIndex, counterId, clientId);
});
} catch {
/**
* Yandex Metrika integration must not affect error reporting.
*/
}
}

/**
* Saves Yandex Metrika counter ID and ClientID under one-based queue position.
*
* @param queueIndex - Zero-based Metrika initialization queue entry index.
* @param counterId - Yandex Metrika counter ID.
* @param clientId - ClientID returned by Yandex Metrika.
*/
private saveCounterClientId(queueIndex: number, counterId: number, clientId: unknown): void {
if (typeof clientId !== 'string' || clientId.length === 0) {
return;
}

this.countersClientIds[queueIndex + 1] = {
counterId,
clientId,
};
}
}
2 changes: 2 additions & 0 deletions packages/browser/src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { DebugAddonMessageProcessor } from './addons/debug-addon-message-processor';
import { BrowserBreadcrumbsMessageProcessor } from './addons/browser-breadcrumbs-message-processor';
import { PerformanceIssuesMonitor } from './addons/performance-issues';
import { YandexMetrikaAddonMessageProcessor } from './addons/yandex-metrika-addon-message-processor';

/**
* Allow to use global VERSION, that will be overwritten by Webpack
Expand Down Expand Up @@ -151,6 +152,7 @@
}

this.addMessageProcessor(new BrowserAddonMessageProcessor());
this.addMessageProcessor(new YandexMetrikaAddonMessageProcessor());

if (this.consoleTracking) {
this.consoleCatcher = ConsoleCatcher.getInstance();
Expand Down Expand Up @@ -224,7 +226,7 @@
* - global errors handling
* - performance issue detectors (Long Tasks / LoAF)
*
* @param settings

Check warning on line 229 in packages/browser/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "settings" description
*/
private configureIssues(settings: HawkInitialSettings): void {
if (settings.issues === false) {
Expand Down
Loading
Loading