initial for v1
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
c770634e73
commit
15e997129a
185
packages/sdks/web/src/index.ts
Normal file
185
packages/sdks/web/src/index.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
import type {
|
||||
OpenPanelOptions as OpenPanelBaseOptions,
|
||||
TrackProperties,
|
||||
} from '@openpanel/sdk';
|
||||
import { OpenPanel as OpenPanelBase } from '@openpanel/sdk';
|
||||
|
||||
export * from '@openpanel/sdk';
|
||||
|
||||
export type OpenPanelOptions = OpenPanelBaseOptions & {
|
||||
trackOutgoingLinks?: boolean;
|
||||
trackScreenViews?: boolean;
|
||||
trackAttributes?: boolean;
|
||||
trackHashChanges?: boolean;
|
||||
};
|
||||
|
||||
function toCamelCase(str: string) {
|
||||
return str.replace(/([-_][a-z])/gi, ($1) =>
|
||||
$1.toUpperCase().replace('-', '').replace('_', '')
|
||||
);
|
||||
}
|
||||
|
||||
export class OpenPanel extends OpenPanelBase {
|
||||
private lastPath = '';
|
||||
private debounceTimer: any;
|
||||
|
||||
constructor(public options: OpenPanelOptions) {
|
||||
super({
|
||||
...options,
|
||||
sdk: 'web',
|
||||
sdkVersion: process.env.WEB_VERSION!,
|
||||
});
|
||||
|
||||
if (!this.isServer()) {
|
||||
this.setGlobalProperties({
|
||||
__referrer: document.referrer,
|
||||
});
|
||||
|
||||
if (this.options.trackScreenViews) {
|
||||
this.trackScreenViews();
|
||||
}
|
||||
|
||||
if (this.options.trackOutgoingLinks) {
|
||||
this.trackOutgoingLinks();
|
||||
}
|
||||
|
||||
if (this.options.trackAttributes) {
|
||||
this.trackAttributes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private debounce(func: () => void, delay: number) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(func, delay);
|
||||
}
|
||||
|
||||
private isServer() {
|
||||
return typeof document === 'undefined';
|
||||
}
|
||||
|
||||
public trackOutgoingLinks() {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const link = target.closest('a');
|
||||
if (link && target) {
|
||||
const href = link.getAttribute('href');
|
||||
if (href?.startsWith('http')) {
|
||||
super.track('link_out', {
|
||||
href,
|
||||
text:
|
||||
link.innerText ||
|
||||
link.getAttribute('title') ||
|
||||
target.getAttribute('alt') ||
|
||||
target.getAttribute('title'),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public trackScreenViews() {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenView();
|
||||
|
||||
const oldPushState = history.pushState;
|
||||
history.pushState = function pushState(...args) {
|
||||
const ret = oldPushState.apply(this, args);
|
||||
window.dispatchEvent(new Event('pushstate'));
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
const oldReplaceState = history.replaceState;
|
||||
history.replaceState = function replaceState(...args) {
|
||||
const ret = oldReplaceState.apply(this, args);
|
||||
window.dispatchEvent(new Event('replacestate'));
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', function () {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
|
||||
const eventHandler = () => this.debounce(() => this.screenView(), 50);
|
||||
|
||||
if (this.options.trackHashChanges) {
|
||||
window.addEventListener('hashchange', eventHandler);
|
||||
} else {
|
||||
window.addEventListener('locationchange', eventHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public trackAttributes() {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const btn = target.closest('button');
|
||||
const anchor = target.closest('a');
|
||||
const element = btn?.getAttribute('data-event')
|
||||
? btn
|
||||
: anchor?.getAttribute('data-event')
|
||||
? anchor
|
||||
: null;
|
||||
if (element) {
|
||||
const properties: Record<string, unknown> = {};
|
||||
for (const attr of element.attributes) {
|
||||
if (attr.name.startsWith('data-') && attr.name !== 'data-event') {
|
||||
properties[toCamelCase(attr.name.replace(/^data-/, ''))] =
|
||||
attr.value;
|
||||
}
|
||||
}
|
||||
const name = element.getAttribute('data-event');
|
||||
if (name) {
|
||||
super.track(name, properties);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
screenView(properties?: TrackProperties): void;
|
||||
screenView(path: string, properties?: TrackProperties): void;
|
||||
screenView(
|
||||
pathOrProperties?: string | TrackProperties,
|
||||
propertiesOrUndefined?: TrackProperties
|
||||
): void {
|
||||
if (this.isServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let path: string;
|
||||
let properties: TrackProperties | undefined;
|
||||
|
||||
if (typeof pathOrProperties === 'string') {
|
||||
path = pathOrProperties;
|
||||
properties = propertiesOrUndefined;
|
||||
} else {
|
||||
path = window.location.href;
|
||||
properties = pathOrProperties;
|
||||
}
|
||||
|
||||
if (this.lastPath === path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPath = path;
|
||||
super.track('screen_view', {
|
||||
...(properties ?? {}),
|
||||
__path: path,
|
||||
__title: document.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user