├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc.toml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── client.ts
├── common.ts
├── constants.ts
├── core.ts
├── index.ts
├── types.ts
├── web-api.ts
└── web.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.json]
15 | indent_size = 2
16 |
17 | [*.cson]
18 | indent_style = tab
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist/
2 | *.cjs
3 | *.js
4 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | plugins: ["@typescript-eslint"],
5 | parserOptions: {
6 | tsconfigRootDir: ".",
7 | project: "tsconfig.json",
8 | },
9 | extends: [
10 | "eslint:recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
13 | "plugin:@typescript-eslint/strict",
14 | "plugin:unicorn/all",
15 | "prettier",
16 | ],
17 | rules: {
18 | "@typescript-eslint/require-await": "off",
19 | "@typescript-eslint/no-unnecessary-condition": "off",
20 | "unicorn/catch-error-name": [
21 | "error",
22 | {
23 | name: "exception",
24 | ignore: [/^error/i, /error$/i, /^exception/i, /exception$/i],
25 | },
26 | ],
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | dist/
4 | tmp/
5 |
--------------------------------------------------------------------------------
/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | arrowParens = 'avoid'
2 | bracketSpacing = true
3 | endOfLine = 'lf'
4 | jsxSingleQuote = true
5 | printWidth = 120
6 | quoteProps = 'as-needed'
7 | semi = true
8 | singleQuote = false
9 | tabWidth = 4
10 | trailingComma = 'es5'
11 | useTabs = false
12 | overrides = [
13 | { files = '*.json', options = { tabWidth = 2 } },
14 | { files = '*.md', options = { tabWidth = 2 } },
15 | ]
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.8.0
4 |
5 | * added funnel endpoints
6 | * updated dependencies
7 |
8 | ## 2.7.0
9 |
10 | * added regional statistics
11 | * removed DNT
12 | * updated dependencies
13 |
14 | ## 2.6.0
15 |
16 | * added tags to page views and filter
17 | * added reading tag statistics
18 | * added meta key-value pairs to filter
19 | * added missing fields to data model
20 | * updated dependencies
21 |
22 | ## 2.5.0
23 |
24 | * added event pages endpoint
25 | * fixed missing title in PirschPageStats
26 | * updated dependencies
27 |
28 | ## 2.4.6
29 |
30 | * updated dependencies
31 |
32 | ## 2.4.5
33 |
34 | * updated dependencies
35 |
36 | ## 2.4.4
37 |
38 | * fixed package structure
39 |
40 | ## 2.4.3
41 |
42 | * fixed package structure
43 |
44 | ## 2.4.2
45 |
46 | * improved inline docs
47 |
48 | ## 2.4.1
49 |
50 | * fixed entrypoint
51 | * fixed configuration validation
52 |
53 | ## 2.4.0
54 |
55 | * added new custom metric fields
56 | * added missing event meta, timezone, offset, sort, direction, and search fields in `PirschFilter`
57 | * removed screen_width and screen_height from `PirschFilter`
58 | * updated dependencies
59 |
60 | ## 2.3.0
61 |
62 | * added optional client hint headers
63 | * updated dependencies
64 |
65 | ## 2.2.1
66 |
67 | * fixed request parameter
68 |
69 | ## 2.2.0
70 |
71 | * improved error handling
72 | * upgraded to TypeScript 5
73 | * updated dependencies
74 |
75 | ## 2.1.3
76 |
77 | * fixed axios request parameters
78 |
79 | ## 2.1.2
80 |
81 | * improved `PirschApiError`
82 | * updated dependencies
83 |
84 | ## 2.1.1
85 |
86 | * fix trustedProxyHeaders
87 | * updated dependencies
88 |
89 | ## 2.1.0
90 |
91 | * added support for batch inserts (page views, events, sessions)
92 | * updated dependencies
93 |
94 | ## 2.0.2
95 |
96 | * fixed build
97 |
98 | ## 2.0.1
99 |
100 | * add title, screen_width and screen_height parameters
101 | * fix trustedProxyHeaders behavior
102 | * remove deprecated cf_connecting_ip, x_forwarded_for, forwarded and x_real_ip fields
103 | * updated dependencies
104 |
105 | ## 2.0.0
106 |
107 | * added support for browsers
108 | * general improvements of the package interface
109 | * updated dependencies
110 |
111 | ## 1.4.1
112 |
113 | * added single access token that don't require to query an access token using oAuth
114 |
115 | ## 1.4.0
116 |
117 | * added hourly visitor statistics, listing events, os and browser versions
118 | * added filter options
119 | * updated return types with new and modified fields
120 | * updated dependencies
121 |
122 | ## 1.3.5
123 |
124 | * fixed domain method returning an object instead of an array
125 |
126 | ## 1.3.4
127 |
128 | * added new hourly visitor statistics
129 | * fixed promise not being rejected in case the token cannot be refreshed
130 |
131 | ## 1.3.3
132 |
133 | * added endpoint for total visitor statistics
134 |
135 | ## 1.3.2
136 |
137 | * fixed copy-paste error
138 |
139 | ## 1.3.1
140 |
141 | * fixed error handling
142 |
143 | ## 1.3.0
144 |
145 | * added endpoint to extend sessions
146 | * added entry page statistics
147 | * added exit page statistics
148 | * added number of sessions to referrer statistics
149 | * added city statistics
150 | * added entry page, exit page, city, and referrer name to filter
151 |
152 | ## 1.2.0
153 |
154 | * added method to send events
155 | * added reading event statistics
156 | * fixed filter parameters to read statistics
157 |
158 | ## 1.1.1
159 |
160 | * fixed build
161 |
162 | ## 1.1.0
163 |
164 | * added `source` and `utm_source` to referrers
165 | * added methods to read statistics
166 | * updated dependencies
167 | * fixed retry on 401
168 |
169 | ## 1.0.1
170 |
171 | * added missing DNT (do not track) header
172 |
173 | ## 1.0.0
174 |
175 | Initial release.
176 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Pirsch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pirsch JavaScript SDK
2 |
3 |
4 |
5 | This is the official JavaScript client SDK for Pirsch. For details, please check out our [documentation](https://docs.pirsch.io/).
6 |
7 | ## Installation
8 |
9 | ```
10 | npm i pirsch-sdk
11 | ```
12 |
13 | ## Usage
14 |
15 | ### Configuration
16 |
17 | The SDK is configured using the constructor. We recommend using an access key instead of a client ID + secret if you only need write access (sending page views and events), as it saves a roundtrip to the server when refreshing the access token.
18 |
19 | If you run your server-side integration behind a proxy or load balancer, make sure you correctly configure `trustedProxyHeaders`. They will be used to extract the real visitor IP for each request. They will be used in the order they are passed into the configuration. Possible values are: `"cf-connecting-ip", "x-forwarded-for", "forwarded", "x-real-ip"`.
20 |
21 | ### Server-Side
22 |
23 | Here is a quick demo on how to use this library in NodeJS:
24 |
25 | ```js
26 | import { createServer } from "node:http";
27 | import { URL } from "node:url";
28 |
29 | // Import the Pirsch client.
30 | import { Pirsch } from "pirsch-sdk";
31 |
32 | // Create a client with the hostname, client ID, and client secret you have configured on the Pirsch dashboard.
33 | const client = new Pirsch({
34 | hostname: "example.com",
35 | protocol: "http", // used to parse the request URL, default is https
36 | clientId: "",
37 | clientSecret: ""
38 | });
39 |
40 | // Create your http handler and start the server.
41 | createServer((request, response) => {
42 | // In this example, we only want to track the / path and nothing else.
43 | // We parse the request URL to read and check the pathname.
44 | const url = new URL(request.url || "", "http://localhost:8765");
45 |
46 | if (url.pathname === "/") {
47 | // Send the hit to Pirsch. hitFromRequest is a helper function that returns all required information from the request.
48 | // You can also built the Hit object on your own and pass it in.
49 | client.hit(client.hitFromRequest(request)).catch(error => {
50 | // Something went wrong, check the error output.
51 | console.error(error);
52 | });
53 | }
54 |
55 | // Render your website...
56 | response.write("Hello from Pirsch!");
57 | response.end();
58 | }).listen(8765);
59 | ```
60 |
61 | ### Client-Side
62 |
63 | Here is how you can do the same in the browser:
64 |
65 | ```js
66 | // Import the Pirsch client.
67 | import { Pirsch } from "pirsch-sdk/web";
68 |
69 | // Create a client with the identification code you have configured on the Pirsch dashboard.
70 | const client = new Pirsch({
71 | identificationCode: ""
72 | });
73 |
74 | const main = async () => {
75 | await client.hit();
76 |
77 | await client.event("test-event", 60, { clicks: 1, test: "xyz" });
78 | }
79 |
80 | void main();
81 | ```
82 |
83 | ## FAQ
84 |
85 | > This module export three Clients (`pirsch-sdk`, `pirsch-sdk/web-api` and `pirsch-sdk/web`), what are the differences?
86 |
87 | - `pirsch-sdk` and `pirsch-sdk/web-api` are based on the same core logic, and function the same. It can be used to access and sending data via the API. `pirsch-sdk/web-api` is a version of the Node client that works in the web. You will rarely need to use this version though.
88 | - `pirsch-sdk/web` is a modular version of the JS Snippet, that has no automatic functionality. You need to send any hits or events yourself.
89 |
90 | > :information_source: Basically your choice will be between `pirsch-sdk` (Node, backend, accessing or sending data) or `pirsch-sdk/web`, (Browser, frontend, sending data) in 99% of the cases.
91 |
92 | ## Changelog
93 |
94 | See [CHANGELOG.md](CHANGELOG.md).
95 |
96 | ## License
97 |
98 | MIT
99 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pirsch-sdk",
3 | "version": "2.8.0",
4 | "description": "TypeScript/JavaScript client SDK for Pirsch.",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "scripts": {
8 | "build": "rm -rf dist && tsc",
9 | "watch": "tsc --watch",
10 | "package-dist": "rm -rf dist/ && npm run build && mkdir -p dist/ && cp package.json LICENSE README.md dist/",
11 | "package-link": "npm run package-dist && cd dist/ && npm link",
12 | "release": "npm run package-dist && npm publish dist/"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/pirsch-analytics/pirsch-js-sdk.git"
17 | },
18 | "keywords": [
19 | "pirsch",
20 | "analytics",
21 | "client",
22 | "sdk",
23 | "api"
24 | ],
25 | "author": "Emvi Software GmbH",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/pirsch-analytics/pirsch-js-sdk/issues"
29 | },
30 | "homepage": "https://pirsch.io/",
31 | "dependencies": {
32 | "@types/node": "^22.5.0",
33 | "axios": "^1.7.4"
34 | },
35 | "devDependencies": {
36 | "@typescript-eslint/eslint-plugin": "^7.10.0",
37 | "@typescript-eslint/parser": "^7.10.0",
38 | "eslint": "^8.56.0",
39 | "eslint-config-prettier": "^9.1.0",
40 | "eslint-plugin-unicorn": "^55.0.0",
41 | "typescript": "^5.5.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { IncomingHttpHeaders, IncomingMessage } from "node:http";
2 | import { URL } from "node:url";
3 |
4 | import axios, { AxiosError as AxiosHttpError, AxiosInstance, AxiosRequestConfig } from "axios";
5 | import {
6 | PirschNodeClientConfig,
7 | PirschHttpOptions,
8 | PirschHit,
9 | PirschProxyHeader,
10 | Optional,
11 | Protocol,
12 | PirschApiErrorResponse,
13 | } from "./types";
14 |
15 | import { PirschCoreClient } from "./core";
16 | import { PirschApiError, PirschUnknownApiError } from "./common";
17 | import { PIRSCH_DEFAULT_PROTOCOL, PIRSCH_REFERRER_QUERY_PARAMETERS, PIRSCH_PROXY_HEADERS } from "./constants";
18 |
19 | /**
20 | * Client is used to access the Pirsch API.
21 | */
22 | export class PirschNodeApiClient extends PirschCoreClient {
23 | protected readonly hostname: string;
24 | protected readonly protocol: Protocol;
25 | protected readonly trustedProxyHeaders?: PirschProxyHeader[];
26 |
27 | private httpClient: AxiosInstance;
28 |
29 | /**
30 | * The constructor creates a new client.
31 | *
32 | * @param {object} configuration You need to pass in the **Hostname**, **Client ID** and **Client Secret** or **Access Key** you have configured on the Pirsch dashboard.
33 | * It's also recommended to set the proper protocol for your website, else it will be set to `https` by default.
34 | * All other configuration parameters can be left to their defaults.
35 | * @param {string} configuration.baseUrl The base URL for the pirsch API
36 | * @param {number} configuration.timeout The default HTTP timeout in milliseconds
37 | * @param {string} configuration.clientId The OAuth client ID
38 | * @param {string} configuration.clientSecret The OAuth client secret
39 | * @param {string} configuration.hostname The hostname of the domain to track
40 | * @param {string} configuration.protocol The default HTTP protocol to use for tracking
41 | *
42 | */
43 | constructor(configuration: PirschNodeClientConfig) {
44 | super(configuration);
45 | const { protocol = PIRSCH_DEFAULT_PROTOCOL, hostname, trustedProxyHeaders } = configuration;
46 |
47 | this.hostname = hostname;
48 | this.protocol = protocol;
49 | this.trustedProxyHeaders = trustedProxyHeaders;
50 |
51 | this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: this.timeout });
52 | }
53 |
54 | /**
55 | * hitFromRequest returns the required data to send a hit to Pirsch for a Node request object.
56 | *
57 | * @param request the Node request object from the http package.
58 | * @returns Hit object containing all necessary fields.
59 | */
60 | public hitFromRequest(request: IncomingMessage): PirschHit {
61 | const url = new URL(request.url ?? "", `${this.protocol}://${this.hostname}`);
62 |
63 | const element: PirschHit = {
64 | url: url.toString(),
65 | ip: request.socket.remoteAddress ?? "",
66 | user_agent: this.getHeader(request.headers, "user-agent") ?? "",
67 | accept_language: this.getHeader(request.headers, "accept-language"),
68 | sec_ch_ua: this.getHeader(request.headers, "Sec-CH-UA"),
69 | sec_ch_ua_mobile: this.getHeader(request.headers, "Sec-CH-UA-Mobile"),
70 | sec_ch_ua_platform: this.getHeader(request.headers, "Sec-CH-UA-Platform"),
71 | sec_ch_ua_platform_version: this.getHeader(request.headers, "Sec-CH-UA-Platform-Version"),
72 | sec_ch_width: this.getHeader(request.headers, "Sec-CH-Width"),
73 | sec_ch_viewport_width: this.getHeader(request.headers, "Sec-CH-Viewport-Width"),
74 | referrer: this.getReferrer(request, url),
75 | };
76 |
77 | if (this.trustedProxyHeaders && this.trustedProxyHeaders.length > 0) {
78 | const header = this.trustedProxyHeaders
79 | .filter(header => {
80 | return PIRSCH_PROXY_HEADERS.includes(header);
81 | })
82 | .find(header => {
83 | return typeof request.headers[header] === "string";
84 | });
85 |
86 | if (header) {
87 | const result = this.getHeader(request.headers, header);
88 |
89 | if (result) {
90 | element.ip = result;
91 | }
92 | }
93 | }
94 |
95 | return element;
96 | }
97 |
98 | private getReferrer(request: IncomingMessage, url: URL): string {
99 | const referrer =
100 | this.getHeader(request.headers, "referer") ?? this.getHeader(request.headers, "referrer") ?? "";
101 |
102 | if (referrer === "") {
103 | for (const parameterName of PIRSCH_REFERRER_QUERY_PARAMETERS) {
104 | const parameter = url.searchParams.get(parameterName);
105 |
106 | if (parameter && parameter !== "") {
107 | return parameter;
108 | }
109 | }
110 | }
111 |
112 | return referrer;
113 | }
114 |
115 | private getHeader(headers: IncomingHttpHeaders, name: string): Optional {
116 | const header = headers[name];
117 |
118 | if (Array.isArray(header)) {
119 | return header.at(0);
120 | }
121 |
122 | return header;
123 | }
124 |
125 | protected async get(url: string, options?: PirschHttpOptions): Promise {
126 | const result = await this.httpClient.get(url, this.httpOptionsToAxiosOptions(options));
127 |
128 | return result.data;
129 | }
130 |
131 | protected async post(
132 | url: string,
133 | data: Data,
134 | options?: PirschHttpOptions
135 | ): Promise {
136 | const result = await this.httpClient.post(url, data, this.httpOptionsToAxiosOptions(options));
137 | return result.data;
138 | }
139 |
140 | protected async toApiError(error: unknown): Promise {
141 | if (error instanceof PirschApiError) {
142 | return error;
143 | }
144 |
145 | if (error instanceof AxiosHttpError && error.response !== undefined) {
146 | const exception = error as AxiosHttpError;
147 |
148 | return new PirschApiError(
149 | exception.response?.status ?? 500,
150 | exception.response?.data ?? { validation: {}, error: [] }
151 | );
152 | }
153 |
154 | if (typeof error === 'object' && error !== null && 'response' in error && typeof error.response === 'object' && error.response !== null && 'status' in error.response && 'data' in error.response) {
155 | return new PirschApiError(error.response.status as number ?? 400, error.response?.data as PirschApiErrorResponse);
156 | }
157 |
158 | if (error instanceof Error) {
159 | return new PirschUnknownApiError(error.message);
160 | }
161 |
162 | if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') {
163 | return new PirschUnknownApiError(error.message);
164 | }
165 |
166 | return new PirschUnknownApiError(JSON.stringify(error));
167 | }
168 |
169 | protected httpOptionsToAxiosOptions(options?: PirschHttpOptions): AxiosRequestConfig {
170 | const result: AxiosRequestConfig = {};
171 |
172 | if (options?.headers) {
173 | result.headers = options.headers;
174 | }
175 |
176 | if (options?.parameters) {
177 | result.params = options.parameters;
178 | }
179 |
180 | return result;
181 | }
182 | }
183 |
184 | export const Pirsch = PirschNodeApiClient;
185 | export const Client = PirschNodeApiClient;
186 |
--------------------------------------------------------------------------------
/src/common.ts:
--------------------------------------------------------------------------------
1 | import { Optional, PirschApiErrorResponse, Scalar } from "./types";
2 | import {
3 | PIRSCH_CLIENT_ID_LENGTH,
4 | PIRSCH_CLIENT_SECRET_LENGTH,
5 | PIRSCH_ACCESS_TOKEN_LENGTH,
6 | PIRSCH_IDENTIFICATION_CODE_LENGTH,
7 | PIRSCH_ACCESS_TOKEN_PREFIX,
8 | } from "./constants";
9 |
10 | export abstract class PirschCommon {
11 | protected assertOauthCredentials({ clientId, clientSecret }: { clientId?: string; clientSecret: string }) {
12 | if (clientId?.length !== PIRSCH_CLIENT_ID_LENGTH) {
13 | throw new Error(`Invalid Client ID, should be of length '${PIRSCH_CLIENT_ID_LENGTH}'!`);
14 | }
15 |
16 | if (clientSecret.length !== PIRSCH_CLIENT_SECRET_LENGTH) {
17 | throw new Error(`Invalid Client ID, should be of length '${PIRSCH_CLIENT_ID_LENGTH}'!`);
18 | }
19 | }
20 |
21 | protected assertAccessTokenCredentials({ accessToken }: { accessToken: string }) {
22 | if (!accessToken.startsWith(PIRSCH_ACCESS_TOKEN_PREFIX)) {
23 | throw new Error(`Invalid Access Token, should start with '${PIRSCH_ACCESS_TOKEN_PREFIX}'!`);
24 | }
25 |
26 | if (accessToken.length !== PIRSCH_ACCESS_TOKEN_LENGTH + PIRSCH_ACCESS_TOKEN_PREFIX.length) {
27 | throw new Error(`Invalid Access Token, should be of length '${PIRSCH_ACCESS_TOKEN_LENGTH}'!`);
28 | }
29 | }
30 |
31 | protected assertIdentificationCodeCredentials({ identificationCode }: { identificationCode: string }) {
32 | if (identificationCode.length !== PIRSCH_IDENTIFICATION_CODE_LENGTH) {
33 | throw new Error(`Invalid Identification Code, should be of length '${PIRSCH_IDENTIFICATION_CODE_LENGTH}'!`);
34 | }
35 | }
36 |
37 | protected prepareScalarObject(value?: Record): Optional> {
38 | if (!value) {
39 | return value;
40 | }
41 |
42 | return Object.fromEntries(
43 | Object.entries(value).map(([key, value]) => {
44 | if (typeof value === "string") {
45 | return [key, value];
46 | }
47 |
48 | return [key, value.toString()];
49 | })
50 | );
51 | }
52 | }
53 |
54 | export class PirschApiError extends Error {
55 | public code: number;
56 | public data: PirschApiErrorResponse;
57 |
58 | public constructor(code: number, data: PirschApiErrorResponse) {
59 | const message = data?.error?.at(0) ??
60 | (data?.validation ? `validation error (${code}): ${JSON.stringify(data.validation)}` : undefined) ??
61 | (code === 404 ? "not found" : undefined) ??
62 | `status ${code}: an unknown error occurred!`;
63 | super(message);
64 | this.name = "PirschApiError";
65 | this.code = code;
66 | this.data = data;
67 | }
68 | }
69 |
70 | export class PirschDomainNotFoundApiError extends PirschApiError {
71 | public constructor() {
72 | const error = ["domain not found!"];
73 | super(404, { error });
74 | this.name = "PirschDomainNotFoundApiError";
75 | }
76 | }
77 |
78 | export class PirschInvalidAccessModeApiError extends PirschApiError {
79 | public constructor(methodName: string) {
80 | const error = [
81 | `you are trying to run the data-accessing method '${methodName}', which is not possible with access tokens. please use a oauth id and secret!`,
82 | ];
83 | super(401, { error });
84 | this.name = "PirschInvalidAccessModeApiError";
85 | }
86 | }
87 |
88 | export class PirschUnknownApiError extends PirschApiError {
89 | public constructor(message: string) {
90 | const error = [message];
91 | super(500, { error });
92 | this.name = "PirschUnknownApiError";
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const PIRSCH_DEFAULT_BASE_URL = "https://api.pirsch.io" as const;
2 | export const PIRSCH_DEFAULT_TIMEOUT = 5000 as const;
3 | export const PIRSCH_DEFAULT_PROTOCOL = "https" as const;
4 |
5 | export const PIRSCH_REFERRER_QUERY_PARAMETERS = ["ref", "referer", "referrer", "source", "utm_source"] as const;
6 |
7 | export const PIRSCH_PROXY_HEADERS = ["cf-connecting-ip", "x-forwarded-for", "forwarded", "x-real-ip"] as const;
8 |
9 | export const PIRSCH_ACCESS_TOKEN_PREFIX = "pa_" as const;
10 |
11 | export const PIRSCH_CLIENT_ID_LENGTH = 32 as const;
12 | export const PIRSCH_CLIENT_SECRET_LENGTH = 64 as const;
13 | export const PIRSCH_ACCESS_TOKEN_LENGTH = 45 as const;
14 | export const PIRSCH_IDENTIFICATION_CODE_LENGTH = 32 as const;
15 |
16 | export const PIRSCH_URL_LENGTH_LIMIT = 1800 as const;
17 |
18 | export enum PirschEndpoint {
19 | AUTHENTICATION = "token",
20 | HIT = "hit",
21 | HIT_BATCH = "hit/batch",
22 | EVENT = "event",
23 | EVENT_BATCH = "event/batch",
24 | SESSION = "session",
25 | SESSION_BATCH = "session/batch",
26 | DOMAIN = "domain",
27 | SESSION_DURATION = "statistics/duration/session",
28 | TIME_ON_PAGE = "statistics/duration/page",
29 | UTM_SOURCE = "statistics/utm/source",
30 | UTM_MEDIUM = "statistics/utm/medium",
31 | UTM_CAMPAIGN = "statistics/utm/campaign",
32 | UTM_CONTENT = "statistics/utm/content",
33 | UTM_TERM = "statistics/utm/term",
34 | TOTAL_VISITORS = "statistics/total",
35 | VISITORS = "statistics/visitor",
36 | PAGES = "statistics/page",
37 | ENTRY_PAGES = "statistics/page/entry",
38 | EXIT_PAGES = "statistics/page/exit",
39 | CONVERSION_GOALS = "statistics/goals",
40 | EVENTS = "statistics/events",
41 | EVENT_METADATA = "statistics/event/meta",
42 | LIST_EVENTS = "statistics/event/list",
43 | EVENTS_PAGES = "statistics/event/page",
44 | GROWTH_RATE = "statistics/growth",
45 | ACTIVE_VISITORS = "statistics/active",
46 | TIME_OF_DAY = "statistics/hours",
47 | LANGUAGE = "statistics/language",
48 | REFERRER = "statistics/referrer",
49 | OS = "statistics/os",
50 | OS_VERSION = "statistics/os/version",
51 | BROWSER = "statistics/browser",
52 | BROWSER_VERSION = "statistics/browser/version",
53 | COUNTRY = "statistics/country",
54 | REGION = "statistics/region",
55 | CITY = "statistics/city",
56 | PLATFORM = "statistics/platform",
57 | SCREEN = "statistics/screen",
58 | KEYWORDS = "statistics/keywords",
59 | TAG_KEYS = "statistics/tags",
60 | TAG_DETAILS = "statistics/tag/details",
61 | LIST_FUNNEL = "/api/v1/funnel",
62 | FUNNEL = "/api/v1/statistics/funnel"
63 | }
64 |
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PirschClientConfig,
3 | PirschAuthenticationResponse,
4 | PirschHit,
5 | PirschDomain,
6 | PirschFilter,
7 | PirschKeyword,
8 | PirschScreenClassStats,
9 | PirschPlatformStats,
10 | PirschCountryStats,
11 | PirschBrowserStats,
12 | PirschOSStats,
13 | PirschReferrerStats,
14 | PirschLanguageStats,
15 | PirschVisitorHourStats,
16 | PirschActiveVisitorsData,
17 | PirschGrowth,
18 | PirschConversionGoal,
19 | PirschEventStats,
20 | PirschPageStats,
21 | PirschVisitorStats,
22 | PirschUTMTermStats,
23 | PirschUTMContentStats,
24 | PirschUTMCampaignStats,
25 | PirschUTMMediumStats,
26 | PirschUTMSourceStats,
27 | PirschTimeSpentStats,
28 | PirschTotalVisitorStats,
29 | PirschEventListStats,
30 | PirschOSVersionStats,
31 | PirschBrowserVersionStats,
32 | PirschEntryStats,
33 | PirschExitStats,
34 | PirschCityStats,
35 | PirschHttpOptions,
36 | PirschAccessMode,
37 | Scalar,
38 | Optional,
39 | PirschEvent,
40 | PirschSession,
41 | PirschBatchHit,
42 | PirschBatchSession,
43 | PirschBatchEvent,
44 | TagStats,
45 | PirschRegionStats,
46 | FunnelData,
47 | Funnel,
48 | } from "./types";
49 |
50 | import { PIRSCH_DEFAULT_BASE_URL, PIRSCH_DEFAULT_TIMEOUT, PirschEndpoint } from "./constants";
51 | import { PirschApiError, PirschCommon, PirschDomainNotFoundApiError, PirschInvalidAccessModeApiError } from "./common";
52 |
53 | export abstract class PirschCoreClient extends PirschCommon {
54 | protected readonly version = "v1";
55 | protected readonly endpoint = "api";
56 |
57 | protected readonly clientId?: string;
58 | protected readonly clientSecret?: string;
59 |
60 | protected readonly baseUrl: string;
61 | protected readonly timeout: number;
62 | protected readonly accessMode: PirschAccessMode;
63 |
64 | protected accessToken = "";
65 |
66 | /**
67 | * The constructor creates a new client.
68 | *
69 | * @param {object} configuration You need to pass in the **Client ID** and **Client Secret** or **Access Key** you have configured on the Pirsch dashboard.
70 | * It's also recommended to set the proper protocol for your website, else it will be set to `https` by default.
71 | * All other configuration parameters can be left to their defaults.
72 | * @param {string} configuration.baseUrl The base URL for the pirsch API
73 | * @param {number} configuration.timeout The default HTTP timeout in milliseconds
74 | * @param {string} configuration.clientId The OAuth client ID
75 | * @param {string} configuration.clientSecret The OAuth client secret
76 | * @param {string} configuration.protocol The default HTTP protocol to use for tracking
77 | *
78 | */
79 | constructor(configuration: PirschClientConfig) {
80 | super();
81 |
82 | const { baseUrl = PIRSCH_DEFAULT_BASE_URL, timeout = PIRSCH_DEFAULT_TIMEOUT } = configuration;
83 |
84 | this.baseUrl = baseUrl;
85 | this.timeout = timeout;
86 |
87 | if ("accessToken" in configuration) {
88 | const { accessToken } = configuration;
89 | this.assertAccessTokenCredentials({ accessToken });
90 | this.accessToken = accessToken;
91 | this.accessMode = "access-token";
92 | } else if ("clientId" in configuration || "clientSecret" in configuration) {
93 | const { clientId, clientSecret } = configuration;
94 | this.assertOauthCredentials({ clientId, clientSecret });
95 | this.clientId = clientId;
96 | this.clientSecret = clientSecret;
97 | this.accessMode = "oauth";
98 | } else {
99 | throw new Error(
100 | `Missing credentials, please supply either '${JSON.stringify({
101 | clientId: "value",
102 | clientSecret: "value",
103 | })}' or '${JSON.stringify({
104 | accessToken: "value",
105 | })}'!`
106 | );
107 | }
108 | }
109 |
110 | /**
111 | * hit sends a hit to Pirsch. Make sure you call it in all request handlers you want to track.
112 | * Also, make sure to filter out unwanted pathnames (like /favicon.ico in your root handler for example).
113 | *
114 | * @param hit all required data for the request.
115 | * @returns APIError or an empty promise, in case something went wrong
116 | */
117 | async hit(hit: PirschHit): Promise> {
118 | return await this.performPost(PirschEndpoint.HIT, hit);
119 | }
120 |
121 | /**
122 | * batchHit sends batched hits to Pirsch.
123 | *
124 | * @param hits all required data for the request.
125 | * @returns APIError or an empty promise, in case something went wrong
126 | */
127 | async batchHits(hits: PirschBatchHit[]): Promise> {
128 | return await this.performPost(PirschEndpoint.HIT_BATCH, hits);
129 | }
130 |
131 | /**
132 | * event sends an event to Pirsch. Make sure you call it in all request handlers you want to track.
133 | * Also, make sure to filter out unwanted pathnames (like /favicon.ico in your root handler for example).
134 | *
135 | * @param name the name for the event
136 | * @param hit all required data for the request
137 | * @param duration optional duration for the event
138 | * @param meta optional object containing metadata (only scalar values, like strings, numbers, and booleans)
139 | * @returns APIError or an empty promise, in case something went wrong
140 | */
141 | async event(
142 | name: string,
143 | hit: PirschHit,
144 | duration = 0,
145 | meta?: Record
146 | ): Promise> {
147 | const event: PirschEvent = {
148 | event_name: name,
149 | event_duration: duration,
150 | event_meta: this.prepareScalarObject(meta),
151 | ...hit,
152 | };
153 |
154 | return await this.performPost(PirschEndpoint.EVENT, event);
155 | }
156 |
157 | /**
158 | * batchEvents sends batched events to Pirsch.
159 | *
160 | * @param events all required data for the request.
161 | * @returns APIError or an empty promise, in case something went wrong
162 | */
163 | async batchEvents(
164 | events: {
165 | name: string;
166 | hit: PirschHit;
167 | time: string;
168 | duration?: number;
169 | meta?: Record;
170 | }[]
171 | ): Promise> {
172 | const results = events.map(({ name, hit, time, duration = 0, meta }) => {
173 | const event: PirschBatchEvent = {
174 | event_name: name,
175 | event_duration: duration,
176 | event_meta: this.prepareScalarObject(meta),
177 | time,
178 | ...hit,
179 | };
180 |
181 | return event;
182 | });
183 |
184 | return await this.performPost(PirschEndpoint.EVENT_BATCH, results);
185 | }
186 |
187 | /**
188 | * session keeps a session alive.
189 | *
190 | * @param session all required data for the request.
191 | * @returns APIError or an empty promise, in case something went wrong
192 | */
193 | async session(session: PirschSession): Promise> {
194 | return await this.performPost(PirschEndpoint.SESSION, session);
195 | }
196 |
197 | /**
198 | * batchSessions keeps batched sessions alive.
199 | *
200 | * @param sessions all required data for the request.
201 | * @returns APIError or an empty promise, in case something went wrong
202 | */
203 | async batchSessions(sessions: PirschBatchSession[]): Promise> {
204 | return await this.performPost(PirschEndpoint.SESSION_BATCH, sessions);
205 | }
206 |
207 | /**
208 | * domain returns the domain for this client.
209 | *
210 | * @returns Domain object for this client.
211 | */
212 | async domain(): Promise {
213 | this.accessModeCheck("domain");
214 |
215 | const result = await this.performGet(PirschEndpoint.DOMAIN);
216 |
217 | const error = new PirschDomainNotFoundApiError();
218 |
219 | if (Array.isArray(result) && result.length === 0) {
220 | return error;
221 | } else if (Array.isArray(result)) {
222 | return result.at(0) ?? error;
223 | }
224 |
225 | return result;
226 | }
227 |
228 | /**
229 | * sessionDuration returns the session duration grouped by day.
230 | *
231 | * @param filter used to filter the result set.
232 | */
233 | async sessionDuration(filter: PirschFilter): Promise {
234 | this.accessModeCheck("sessionDuration");
235 |
236 | return await this.performFilteredGet(PirschEndpoint.SESSION_DURATION, filter);
237 | }
238 |
239 | /**
240 | * timeOnPage returns the time spent on pages.
241 | *
242 | * @param filter used to filter the result set.
243 | */
244 | async timeOnPage(filter: PirschFilter): Promise {
245 | this.accessModeCheck("timeOnPage");
246 |
247 | return await this.performFilteredGet(PirschEndpoint.TIME_ON_PAGE, filter);
248 | }
249 |
250 | /**
251 | * utmSource returns the utm sources.
252 | *
253 | * @param filter used to filter the result set.
254 | */
255 | async utmSource(filter: PirschFilter): Promise {
256 | this.accessModeCheck("utmSource");
257 |
258 | return await this.performFilteredGet(PirschEndpoint.UTM_SOURCE, filter);
259 | }
260 |
261 | /**
262 | * utmMedium returns the utm medium.
263 | *
264 | * @param filter used to filter the result set.
265 | */
266 | async utmMedium(filter: PirschFilter): Promise {
267 | this.accessModeCheck("utmMedium");
268 |
269 | return await this.performFilteredGet(PirschEndpoint.UTM_MEDIUM, filter);
270 | }
271 |
272 | /**
273 | * utmCampaign returns the utm campaigns.
274 | *
275 | * @param filter used to filter the result set.
276 | */
277 | async utmCampaign(filter: PirschFilter): Promise {
278 | this.accessModeCheck("utmCampaign");
279 |
280 | return await this.performFilteredGet(PirschEndpoint.UTM_CAMPAIGN, filter);
281 | }
282 |
283 | /**
284 | * utmContent returns the utm content.
285 | *
286 | * @param filter used to filter the result set.
287 | */
288 | async utmContent(filter: PirschFilter): Promise {
289 | this.accessModeCheck("utmContent");
290 |
291 | return await this.performFilteredGet(PirschEndpoint.UTM_CONTENT, filter);
292 | }
293 |
294 | /**
295 | * utmTerm returns the utm term.
296 | *
297 | * @param filter used to filter the result set.
298 | */
299 | async utmTerm(filter: PirschFilter): Promise {
300 | this.accessModeCheck("utmTerm");
301 |
302 | return await this.performFilteredGet(PirschEndpoint.UTM_TERM, filter);
303 | }
304 |
305 | /**
306 | * totalVisitors returns the total visitor statistics.
307 | *
308 | * @param filter used to filter the result set.
309 | */
310 | async totalVisitors(filter: PirschFilter): Promise {
311 | this.accessModeCheck("totalVisitors");
312 |
313 | return await this.performFilteredGet(PirschEndpoint.TOTAL_VISITORS, filter);
314 | }
315 |
316 | /**
317 | * visitors returns the visitor statistics grouped by day.
318 | *
319 | * @param filter used to filter the result set.
320 | */
321 | async visitors(filter: PirschFilter): Promise {
322 | this.accessModeCheck("visitors");
323 |
324 | return await this.performFilteredGet(PirschEndpoint.VISITORS, filter);
325 | }
326 |
327 | /**
328 | * entryPages returns the entry page statistics grouped by page.
329 | *
330 | * @param filter used to filter the result set.
331 | */
332 | async entryPages(filter: PirschFilter): Promise {
333 | this.accessModeCheck("entryPages");
334 |
335 | return await this.performFilteredGet(PirschEndpoint.ENTRY_PAGES, filter);
336 | }
337 |
338 | /**
339 | * exitPages returns the exit page statistics grouped by page.
340 | *
341 | * @param filter used to filter the result set.
342 | */
343 | async exitPages(filter: PirschFilter): Promise {
344 | this.accessModeCheck("exitPages");
345 |
346 | return await this.performFilteredGet(PirschEndpoint.EXIT_PAGES, filter);
347 | }
348 |
349 | /**
350 | * pages returns the page statistics grouped by page.
351 | *
352 | * @param filter used to filter the result set.
353 | */
354 | async pages(filter: PirschFilter): Promise {
355 | this.accessModeCheck("pages");
356 |
357 | return await this.performFilteredGet(PirschEndpoint.PAGES, filter);
358 | }
359 |
360 | /**
361 | * conversionGoals returns all conversion goals.
362 | *
363 | * @param filter used to filter the result set.
364 | */
365 | async conversionGoals(filter: PirschFilter): Promise {
366 | this.accessModeCheck("conversionGoals");
367 |
368 | return await this.performFilteredGet(PirschEndpoint.CONVERSION_GOALS, filter);
369 | }
370 |
371 | /**
372 | * events returns all events.
373 | *
374 | * @param filter used to filter the result set.
375 | */
376 | async events(filter: PirschFilter): Promise {
377 | this.accessModeCheck("events");
378 |
379 | return await this.performFilteredGet(PirschEndpoint.EVENTS, filter);
380 | }
381 |
382 | /**
383 | * eventMetadata returns the metadata for a single event.
384 | * The event name and metadata key must be set in the filter, or otherwise no results will be returned.
385 | *
386 | * @param filter used to filter the result set.
387 | */
388 | async eventMetadata(filter: PirschFilter): Promise {
389 | this.accessModeCheck("eventMetadata");
390 |
391 | return await this.performFilteredGet(PirschEndpoint.EVENT_METADATA, filter);
392 | }
393 |
394 | /**
395 | * listEvents returns a list of all events including metadata.
396 | *
397 | * @param filter used to filter the result set.
398 | */
399 | async listEvents(filter: PirschFilter): Promise {
400 | this.accessModeCheck("listEvents");
401 |
402 | return await this.performFilteredGet(PirschEndpoint.LIST_EVENTS, filter);
403 | }
404 |
405 | /**
406 | * eventPages returns all pages an event has been triggered on.
407 | * The event name must be set in the filter, or otherwise no results will be returned.
408 | *
409 | * @param filter used to filter the result set.
410 | */
411 | async eventPages(filter: PirschFilter): Promise {
412 | this.accessModeCheck("eventPages");
413 |
414 | return await this.performFilteredGet(PirschEndpoint.EVENTS_PAGES, filter);
415 | }
416 |
417 | /**
418 | * growth returns the growth rates for visitors, bounces, ...
419 | *
420 | * @param filter used to filter the result set.
421 | */
422 | async growth(filter: PirschFilter): Promise {
423 | this.accessModeCheck("growth");
424 |
425 | return await this.performFilteredGet(PirschEndpoint.GROWTH_RATE, filter);
426 | }
427 |
428 | /**
429 | * activeVisitors returns the active visitors and what pages they're on.
430 | *
431 | * @param filter used to filter the result set.
432 | */
433 | async activeVisitors(filter: PirschFilter): Promise {
434 | this.accessModeCheck("activeVisitors");
435 |
436 | return await this.performFilteredGet(PirschEndpoint.ACTIVE_VISITORS, filter);
437 | }
438 |
439 | /**
440 | * timeOfDay returns the number of unique visitors grouped by time of day.
441 | *
442 | * @param filter used to filter the result set.
443 | */
444 | async timeOfDay(filter: PirschFilter): Promise {
445 | this.accessModeCheck("timeOfDay");
446 |
447 | return await this.performFilteredGet(PirschEndpoint.TIME_OF_DAY, filter);
448 | }
449 |
450 | /**
451 | * languages returns language statistics.
452 | *
453 | * @param filter used to filter the result set.
454 | */
455 | async languages(filter: PirschFilter): Promise {
456 | this.accessModeCheck("languages");
457 |
458 | return await this.performFilteredGet(PirschEndpoint.LANGUAGE, filter);
459 | }
460 |
461 | /**
462 | * referrer returns referrer statistics.
463 | *
464 | * @param filter used to filter the result set.
465 | */
466 | async referrer(filter: PirschFilter): Promise {
467 | this.accessModeCheck("referrer");
468 |
469 | return await this.performFilteredGet(PirschEndpoint.REFERRER, filter);
470 | }
471 |
472 | /**
473 | * os returns operating system statistics.
474 | *
475 | * @param filter used to filter the result set.
476 | */
477 | async os(filter: PirschFilter): Promise {
478 | this.accessModeCheck("os");
479 |
480 | return await this.performFilteredGet(PirschEndpoint.OS, filter);
481 | }
482 |
483 | /**
484 | * osVersions returns operating system version statistics.
485 | *
486 | * @param filter used to filter the result set.
487 | */
488 | async osVersions(filter: PirschFilter): Promise {
489 | this.accessModeCheck("osVersions");
490 |
491 | return await this.performFilteredGet(PirschEndpoint.OS_VERSION, filter);
492 | }
493 |
494 | /**
495 | * browser returns browser statistics.
496 | *
497 | * @param filter used to filter the result set.
498 | */
499 | async browser(filter: PirschFilter): Promise {
500 | this.accessModeCheck("browser");
501 |
502 | return await this.performFilteredGet(PirschEndpoint.BROWSER, filter);
503 | }
504 |
505 | /**
506 | * browserVersions returns browser version statistics.
507 | *
508 | * @param filter used to filter the result set.
509 | */
510 | async browserVersions(filter: PirschFilter): Promise {
511 | this.accessModeCheck("browserVersions");
512 |
513 | return await this.performFilteredGet(PirschEndpoint.BROWSER_VERSION, filter);
514 | }
515 |
516 | /**
517 | * country returns country statistics.
518 | *
519 | * @param filter used to filter the result set.
520 | */
521 | async country(filter: PirschFilter): Promise {
522 | this.accessModeCheck("country");
523 |
524 | return await this.performFilteredGet(PirschEndpoint.COUNTRY, filter);
525 | }
526 |
527 | /**
528 | * region returns regional statistics.
529 | *
530 | * @param filter used to filter the result set.
531 | */
532 | async region(filter: PirschFilter): Promise {
533 | this.accessModeCheck("region");
534 |
535 | return await this.performFilteredGet(PirschEndpoint.REGION, filter);
536 | }
537 |
538 | /**
539 | * city returns city statistics.
540 | *
541 | * @param filter used to filter the result set.
542 | */
543 | async city(filter: PirschFilter): Promise {
544 | this.accessModeCheck("city");
545 |
546 | return await this.performFilteredGet(PirschEndpoint.CITY, filter);
547 | }
548 |
549 | /**
550 | * platform returns the platforms used by visitors.
551 | *
552 | * @param filter used to filter the result set.
553 | */
554 | async platform(filter: PirschFilter): Promise {
555 | this.accessModeCheck("platform");
556 |
557 | return await this.performFilteredGet(PirschEndpoint.PLATFORM, filter);
558 | }
559 |
560 | /**
561 | * screen returns the screen classes used by visitors.
562 | *
563 | * @param filter used to filter the result set.
564 | */
565 | async screen(filter: PirschFilter): Promise {
566 | this.accessModeCheck("screen");
567 |
568 | return await this.performFilteredGet(PirschEndpoint.SCREEN, filter);
569 | }
570 |
571 | /**
572 | * screen returns the screen classes used by visitors.
573 | *
574 | * @param filter used to filter the result set.
575 | */
576 | async tagKeys(filter: PirschFilter): Promise {
577 | this.accessModeCheck("tag_keys");
578 |
579 | return await this.performFilteredGet(PirschEndpoint.TAG_KEYS, filter);
580 | }
581 |
582 | /**
583 | * screen returns the screen classes used by visitors.
584 | *
585 | * @param filter used to filter the result set.
586 | */
587 | async tags(filter: PirschFilter): Promise {
588 | this.accessModeCheck("tags");
589 |
590 | return await this.performFilteredGet(PirschEndpoint.TAG_DETAILS, filter);
591 | }
592 |
593 | /**
594 | * keywords returns the Google keywords, rank, and CTR.
595 | *
596 | * @param filter used to filter the result set.
597 | */
598 | async keywords(filter: PirschFilter): Promise {
599 | this.accessModeCheck("keywords");
600 |
601 | return await this.performFilteredGet(PirschEndpoint.KEYWORDS, filter);
602 | }
603 |
604 | /**
605 | * listFunnel returns a list of all funnels including step definition for given domain ID.
606 | *
607 | * @param filter used to filter the result set.
608 | */
609 | async listFunnel(filter: PirschFilter): Promise {
610 | this.accessModeCheck("listFunnel");
611 |
612 | return await this.performFilteredGet(PirschEndpoint.LIST_FUNNEL, filter);
613 | }
614 |
615 | /**
616 | * funnel returns a list of all funnels including step definition for given domain ID.
617 | *
618 | * @param filter used to filter the result set. Then funnel_id must be set.
619 | */
620 | async funnel(filter: PirschFilter): Promise {
621 | this.accessModeCheck("funnel");
622 |
623 | return await this.performFilteredGet(PirschEndpoint.FUNNEL, filter);
624 | }
625 |
626 | private async performPost(
627 | path: PirschEndpoint,
628 | data: T,
629 | retry = true
630 | ): Promise> {
631 | try {
632 | await this.post(this.generateUrl(path), data, {
633 | headers: {
634 | "Content-Type": "application/json",
635 | Authorization: `Bearer ${this.accessToken}`,
636 | },
637 | });
638 | return;
639 | } catch (error: unknown) {
640 | const exception = await this.toApiError(error);
641 |
642 | if (this.accessMode === "oauth" && exception.code === 401 && retry) {
643 | await this.refreshToken();
644 | return this.performPost(path, data, false);
645 | }
646 |
647 | throw exception;
648 | }
649 | }
650 |
651 | private async performGet(
652 | path: PirschEndpoint,
653 | parameters: object = {},
654 | retry = true
655 | ): Promise {
656 | try {
657 | if (!this.accessToken && retry) {
658 | await this.refreshToken();
659 | }
660 |
661 | const data = await this.get(this.generateUrl(path), {
662 | headers: {
663 | "Content-Type": "application/json",
664 | Authorization: `Bearer ${this.accessToken}`,
665 | },
666 | parameters,
667 | });
668 |
669 | return data;
670 | } catch (error: unknown) {
671 | const exception = await this.toApiError(error);
672 |
673 | if (this.accessMode === "oauth" && exception.code === 401 && retry) {
674 | await this.refreshToken();
675 | return this.performGet(path, parameters, false);
676 | }
677 |
678 | throw exception;
679 | }
680 | }
681 |
682 | private async performFilteredGet(url: PirschEndpoint, filter: PirschFilter): Promise {
683 | return this.performGet(url, this.getFilterParams(filter));
684 | }
685 |
686 | private getFilterParams(filter: PirschFilter): Record {
687 | const params: Record = {
688 | id: filter.id,
689 | from: filter.from,
690 | to: filter.to,
691 | start: filter.start,
692 | scale: filter.scale,
693 | tz: filter.tz,
694 | path: filter.path,
695 | pattern: filter.pattern,
696 | entry_path: filter.entry_path,
697 | exit_path: filter.exit_path,
698 | event: filter.event,
699 | event_meta_key: filter.event_meta_key,
700 | language: filter.language,
701 | country: filter.country,
702 | region: filter.region,
703 | city: filter.city,
704 | referrer: filter.referrer,
705 | referrer_name: filter.referrer_name,
706 | os: filter.os,
707 | browser: filter.browser,
708 | platform: filter.platform,
709 | screen_class: filter.screen_class,
710 | utm_source: filter.utm_source,
711 | utm_medium: filter.utm_medium,
712 | utm_campaign: filter.utm_campaign,
713 | utm_content: filter.utm_content,
714 | utm_term: filter.utm_term,
715 | tag: filter.tag,
716 | custom_metric_key: filter.custom_metric_key,
717 | custom_metric_type: filter.custom_metric_type,
718 | search: filter.search,
719 | offset: filter.offset,
720 | limit: filter.limit,
721 | sort: filter.sort,
722 | direction: filter.direction,
723 | include_avg_time_on_page: filter.include_avg_time_on_page
724 | };
725 |
726 | if (filter.event_meta) {
727 | for (const [key, value] of Object.entries(filter.event_meta)) {
728 | params[`meta_${key}`] = value;
729 | }
730 | }
731 |
732 | if (filter.tags) {
733 | for (const [key, value] of Object.entries(filter.tags)) {
734 | params[`tag_${key}`] = value;
735 | }
736 | }
737 |
738 | return params;
739 | }
740 |
741 | private async refreshToken(): Promise> {
742 | if (this.accessMode === "access-token") {
743 | return;
744 | }
745 |
746 | try {
747 | const result = await this.post(
748 | this.generateUrl(PirschEndpoint.AUTHENTICATION),
749 | {
750 | client_id: this.clientId,
751 | client_secret: this.clientSecret,
752 | }
753 | );
754 | this.accessToken = result.access_token;
755 | return;
756 | } catch (error: unknown) {
757 | this.accessToken = "";
758 |
759 | const exception = await this.toApiError(error);
760 |
761 | return exception;
762 | }
763 | }
764 |
765 | private generateUrl(path: PirschEndpoint): string {
766 | return "/" + [this.endpoint, this.version, path].join("/");
767 | }
768 |
769 | private accessModeCheck(methodName: string) {
770 | if (this.accessMode === "access-token") {
771 | throw new PirschInvalidAccessModeApiError(methodName);
772 | }
773 | }
774 |
775 | protected abstract post(
776 | url: string,
777 | data: Data,
778 | options?: PirschHttpOptions
779 | ): Promise;
780 | protected abstract get(url: string, options?: PirschHttpOptions): Promise;
781 | protected abstract toApiError(error: unknown): Promise;
782 | }
783 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./types";
2 | export * from "./constants";
3 | export * from "./client";
4 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { PIRSCH_PROXY_HEADERS } from "./constants";
2 |
3 | /**
4 | * PirschClientConfigBase contains the base configuration parameters for the Client.
5 | */
6 | export interface PirschClientConfigBase {
7 | /**
8 | * The base URL for the pirsch API
9 | *
10 | * @default 'https://api.pirsch.io'
11 | */
12 | baseUrl?: string;
13 | /**
14 | * The default HTTP timeout in milliseconds
15 | *
16 | * @default 5000
17 | */
18 | timeout?: number;
19 | }
20 |
21 | /**
22 | * PirschOAuthClientConfig contains the configuration parameters for the Client.
23 | */
24 | export interface PirschOAuthClientConfig extends PirschClientConfigBase {
25 | /**
26 | * The OAuth client ID
27 | */
28 | clientId?: string;
29 | /**
30 | * The OAuth client secret
31 | */
32 | clientSecret: string;
33 | }
34 |
35 | /**
36 | * PirschTokenClientConfig contains the configuration parameters for the Client.
37 | */
38 | export interface PirschTokenClientConfig extends PirschClientConfigBase {
39 | /**
40 | * The secret access token
41 | */
42 | accessToken: string;
43 | }
44 |
45 | /**
46 | * IdentificationCode contains the configuration parameters for the Client.
47 | */
48 | export interface PirschIdentificationCodeClientConfig extends PirschClientConfigBase {
49 | /**
50 | * The public identification code
51 | */
52 | identificationCode: string;
53 | /**
54 | * The hostname of the domain to track
55 | */
56 | hostname?: string;
57 | }
58 |
59 | export interface PirschNodeClientConfigBase {
60 | /**
61 | * The hostname of the domain to track
62 | */
63 | hostname: string;
64 | /**
65 | * The default HTTP protocol to use for tracking
66 | *
67 | * @default 'https'
68 | */
69 | protocol?: Protocol;
70 | /**
71 | * The proxy headers to trust
72 | *
73 | * @default undefined
74 | */
75 | trustedProxyHeaders?: PirschProxyHeader[];
76 | }
77 |
78 | export type PirschClientConfig = PirschOAuthClientConfig | PirschTokenClientConfig;
79 | export type PirschNodeClientConfig =
80 | | (PirschOAuthClientConfig & PirschNodeClientConfigBase)
81 | | (PirschTokenClientConfig & PirschNodeClientConfigBase);
82 |
83 | /**
84 | * PirschAuthenticationResponse is the authentication response for the API and returns the access token.
85 | */
86 | export interface PirschAuthenticationResponse {
87 | access_token: string;
88 | }
89 |
90 | /**
91 | * PirschHit contains all required fields to send a hit to Pirsch. The URL, IP, and User-Agent are mandatory,
92 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
93 | * The fields can be set from the request headers.
94 | */
95 | export interface PirschHit {
96 | url: string;
97 | ip: string;
98 | user_agent: string;
99 | accept_language?: string;
100 | sec_ch_ua?: string;
101 | sec_ch_ua_mobile?: string;
102 | sec_ch_ua_platform?: string;
103 | sec_ch_ua_platform_version?: string;
104 | sec_ch_width?: string;
105 | sec_ch_viewport_width?: string;
106 | title?: string;
107 | referrer?: string;
108 | screen_width?: number;
109 | screen_height?: number;
110 | tags?: Record;
111 | }
112 |
113 | /**
114 | * PirschBatchHit contains all required fields to send batched hits to Pirsch. The URL, IP, and User-Agent are mandatory,
115 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
116 | * The fields can be set from the request headers.
117 | */
118 | export interface PirschBatchHit extends PirschHit {
119 | time: string;
120 | }
121 |
122 | /**
123 | * PirschEvent contains all required fields to send a event to Pirsch. The Name, URL, IP, and User-Agent are mandatory,
124 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
125 | * The fields can be set from the request headers.
126 | */
127 | export interface PirschEvent extends PirschHit {
128 | event_name: string;
129 | event_duration?: number;
130 | event_meta?: Record;
131 | }
132 |
133 | /**
134 | * PirschBatchEvent contains all required fields to send batched events to Pirsch. The Name, URL, IP, and User-Agent are mandatory,
135 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
136 | * The fields can be set from the request headers.
137 | */
138 | export interface PirschBatchEvent extends PirschEvent {
139 | time: string;
140 | }
141 |
142 | /**
143 | * PirschSession contains all required fields to send a session to Pirsch. The IP and User-Agent are mandatory,
144 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
145 | * The fields can be set from the request headers.
146 | */
147 | export type PirschSession = Pick;
148 |
149 | /**
150 | * PirschBatchSession contains all required fields to send batched sessions to Pirsch. The IP and User-Agent are mandatory,
151 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
152 | * The fields can be set from the request headers.
153 | */
154 | export interface PirschBatchSession extends PirschSession {
155 | time: string;
156 | }
157 |
158 | /**
159 | * PirschBrowserHit contains all required fields to send a browser hit to Pirsch. The URL and User-Agent are mandatory,
160 | * all other fields can be left empty, but it's highly recommended to send all fields to generate reliable data.
161 | * The fields can be set from the request headers.
162 | */
163 | export interface PirschBrowserHit {
164 | url: string;
165 | title?: string;
166 | accept_language?: string;
167 | referrer?: string;
168 | screen_width?: number;
169 | screen_height?: number;
170 | tags?: Record;
171 | }
172 |
173 | /**
174 | * PirschApiErrorResponse represents an error returned from the API.
175 | */
176 | export interface PirschApiErrorResponse {
177 | validation?: PirschValidation;
178 | error: string[];
179 | }
180 |
181 | /**
182 | * PirschValidation is a validation error string for a specific field.
183 | */
184 | export type PirschValidation = Record;
185 |
186 | /**
187 | * PirschScale sets the time period over which data is aggregated by.
188 | */
189 | export type PirschScale = "day" | "week" | "month" | "year";
190 |
191 | /**
192 | * CustomMetricType sets the custom metric aggregation type.
193 | */
194 | export type PirschCustomMetricType = "integer" | "float";
195 |
196 | /**
197 | * PirschSortDirection is used to sort results.
198 | */
199 | export type PirschSortDirection = "asc" | "desc";
200 |
201 | /**
202 | * PirschFilter is used to filter statistics.
203 | * DomainID, From, and To are required dates (the time is ignored).
204 | */
205 | export interface PirschFilter {
206 | id: string;
207 | from: Date;
208 | to: Date;
209 | start?: number;
210 | scale?: PirschScale;
211 | tz?: string;
212 | path?: string;
213 | pattern?: string;
214 | entry_path?: string;
215 | exit_path?: string;
216 | event?: string;
217 | event_meta_key?: string;
218 | event_meta?: Record;
219 | language?: string;
220 | country?: string;
221 | region?: string;
222 | city?: string;
223 | referrer?: string;
224 | referrer_name?: string;
225 | os?: string;
226 | browser?: string;
227 | platform?: string;
228 | screen_class?: string;
229 | utm_source?: string;
230 | utm_medium?: string;
231 | utm_campaign?: string;
232 | utm_content?: string;
233 | utm_term?: string;
234 | tag?: string;
235 | tags?: Record;
236 | custom_metric_key?: string;
237 | custom_metric_type?: PirschCustomMetricType;
238 | search?: string;
239 | offset?: number;
240 | limit?: number;
241 | sort?: string;
242 | direction?: PirschSortDirection;
243 | include_avg_time_on_page?: boolean;
244 | funnel_id?: string;
245 | }
246 |
247 | /**
248 | * PirschBaseEntity contains the base data for all entities.
249 | */
250 | export interface PirschBaseEntity {
251 | id: string;
252 | def_time: Date;
253 | mod_time: Date;
254 | }
255 |
256 | /**
257 | * PirschDomain is a domain on the dashboard.
258 | */
259 | export interface PirschDomain extends PirschBaseEntity {
260 | user_id: string;
261 | organization_id: string;
262 | hostname: string;
263 | subdomain: string;
264 | identification_code: string;
265 | public: boolean;
266 | google_user_id?: string;
267 | google_user_email?: string;
268 | gsc_domain?: string;
269 | new_owner?: number;
270 | timezone?: string;
271 | group_by_title: boolean;
272 | active_visitors_seconds?: number;
273 | disable_scripts: boolean;
274 | statistics_start?: Date;
275 | imported_statistics: boolean;
276 | theme_id: string;
277 | theme: Object;
278 | custom_domain?: string;
279 | display_name?: string;
280 | user_role: string;
281 | settings: Object;
282 | theme_settings: Object;
283 | pinned: boolean;
284 | subscription_active: boolean;
285 | }
286 |
287 | /**
288 | * PirschTimeSpentStats is the time spent on the website or specific pages.
289 | */
290 | export interface PirschTimeSpentStats {
291 | day?: Date;
292 | week?: Date;
293 | month?: Date;
294 | year?: Date;
295 | path: string;
296 | title: string;
297 | average_time_spent_seconds: number;
298 | }
299 |
300 | /**
301 | * PirschMetaStats is the base for meta result types (languages, countries, ...).
302 | */
303 | export interface PirschMetaStats {
304 | visitors: number;
305 | relative_visitors: number;
306 | }
307 |
308 | /**
309 | * PirschUTMSourceStats is the result export interface for utm source statistics.
310 | */
311 | export interface PirschUTMSourceStats extends PirschMetaStats {
312 | utm_source: string;
313 | }
314 |
315 | /**
316 | * PirschUTMMediumStats is the result export interface for utm medium statistics.
317 | */
318 | export interface PirschUTMMediumStats extends PirschMetaStats {
319 | utm_medium: string;
320 | }
321 |
322 | /**
323 | * PirschUTMCampaignStats is the result export interface for utm campaign statistics.
324 | */
325 | export interface PirschUTMCampaignStats extends PirschMetaStats {
326 | utm_campaign: string;
327 | }
328 |
329 | /**
330 | * PirschUTMContentStats is the result export interface for utm content statistics.
331 | */
332 | export interface PirschUTMContentStats extends PirschMetaStats {
333 | utm_content: string;
334 | }
335 |
336 | /**
337 | * PirschUTMTermStats is the result export interface for utm term statistics.
338 | */
339 | export interface PirschUTMTermStats extends PirschMetaStats {
340 | utm_term: string;
341 | }
342 |
343 | /**
344 | * PirschTotalVisitorStats is the result export interface for total visitor statistics.
345 | */
346 | export interface PirschTotalVisitorStats {
347 | visitors: number;
348 | views: number;
349 | sessions: number;
350 | bounces: number;
351 | bounce_rate: number;
352 | cr: number;
353 | custom_metric_avg: number;
354 | custom_metric_total: number;
355 | }
356 |
357 | /**
358 | * PirschVisitorStats is the result export interface for visitor statistics.
359 | */
360 | export interface PirschVisitorStats {
361 | day?: Date;
362 | week?: Date;
363 | month?: Date;
364 | year?: Date;
365 | visitors: number;
366 | views: number;
367 | sessions: number;
368 | bounces: number;
369 | bounce_rate: number;
370 | cr: number;
371 | custom_metric_avg: number;
372 | custom_metric_total: number;
373 | }
374 |
375 | /**
376 | * PirschPageStats is the result export interface for page statistics.
377 | */
378 | export interface PirschPageStats {
379 | path: string;
380 | title: string;
381 | visitors: number;
382 | views: number;
383 | sessions: number;
384 | bounces: number;
385 | relative_visitors: number;
386 | relative_views: number;
387 | bounce_rate: number;
388 | average_time_spent_seconds: number;
389 | }
390 |
391 | /*
392 | * PirschEntryStats is the result type for entry page statistics.
393 | */
394 | export interface PirschEntryStats {
395 | path: string;
396 | title: string;
397 | visitors: number;
398 | sessions: number;
399 | entries: number;
400 | entry_rate: number;
401 | average_time_spent_seconds: number;
402 | }
403 |
404 | /*
405 | * PirschExitStats is the result type for exit page statistics.
406 | */
407 | export interface PirschExitStats {
408 | exit_path: string;
409 | title: string;
410 | visitors: number;
411 | sessions: number;
412 | exits: number;
413 | exit_rate: number;
414 | }
415 |
416 | /**
417 | * PirschConversionGoal is a conversion goal as configured on the dashboard.
418 | */
419 | export interface PirschConversionGoal extends PirschBaseEntity {
420 | domain_id: string;
421 | name: string;
422 | path_pattern: string;
423 | pattern: string;
424 | visitor_goal?: number;
425 | cr_goal?: number;
426 | delete_reached: boolean;
427 | email_reached: boolean;
428 | }
429 |
430 | // PirschEventStats is the result type for custom events.
431 | export interface PirschEventStats {
432 | name: string;
433 | visitors: number;
434 | views: number;
435 | cr: number;
436 | average_duration_seconds: number;
437 | meta_keys: string[];
438 | meta_value: string;
439 | }
440 |
441 | // PirschEventListStats is the result type for a custom event list.
442 | export interface PirschEventListStats {
443 | name: string;
444 | meta: Record;
445 | visitors: number;
446 | count: number;
447 | }
448 |
449 | /**
450 | * PirschPageConversionsStats is the result export interface for page conversions.
451 | */
452 | export interface PirschPageConversionsStats {
453 | visitors: number;
454 | views: number;
455 | cr: number;
456 | }
457 |
458 | /**
459 | * PirschConversionGoalStats are the statistics for a conversion goal.
460 | */
461 | export interface PirschConversionGoalStats {
462 | page_goal: PirschConversionGoal;
463 | stats: PirschPageConversionsStats;
464 | }
465 |
466 | /**
467 | * PirschGrowth represents the visitors, views, sessions, bounces, and average session duration growth between two time periods.
468 | */
469 | export interface PirschGrowth {
470 | visitors_growth: number;
471 | views_growth: number;
472 | sessions_growth: number;
473 | bounces_growth: number;
474 | time_spent_growth: number;
475 | cr_growth: number;
476 | custom_metric_avg_growth: number;
477 | custom_metric_total_growth: number;
478 | }
479 |
480 | /**
481 | * PirschActiveVisitorStats is the result export interface for active visitor statistics.
482 | */
483 | export interface PirschActiveVisitorStats {
484 | path: string;
485 | title: string;
486 | visitors: number;
487 | }
488 |
489 | /**
490 | * PirschActiveVisitorsData contains the active visitors data.
491 | */
492 | export interface PirschActiveVisitorsData {
493 | stats: PirschActiveVisitorStats[];
494 | visitors: number;
495 | }
496 |
497 | /**
498 | * PirschVisitorHourStats is the result export interface for visitor statistics grouped by time of day.
499 | */
500 | export interface PirschVisitorHourStats {
501 | hour: number;
502 | visitors: number;
503 | views: number;
504 | sessions: number;
505 | bounces: number;
506 | bounce_rate: number;
507 | cr: number;
508 | custom_metric_avg: number;
509 | custom_metric_total: number;
510 | }
511 |
512 | /**
513 | * PirschLanguageStats is the result export interface for language statistics.
514 | */
515 | export interface PirschLanguageStats extends PirschMetaStats {
516 | language: string;
517 | }
518 |
519 | /**
520 | * PirschCountryStats is the result export interface for country statistics.
521 | */
522 | export interface PirschCountryStats extends PirschMetaStats {
523 | country_code: string;
524 | }
525 |
526 | /*
527 | * PirschRegionStats is the result type for regional statistics.
528 | */
529 | export interface PirschRegionStats extends PirschMetaStats {
530 | country_code: string;
531 | region: string;
532 | }
533 |
534 | /*
535 | * PirschCityStats is the result type for city statistics.
536 | */
537 | export interface PirschCityStats extends PirschMetaStats {
538 | country_code: string;
539 | region: string;
540 | city: string;
541 | }
542 |
543 | /**
544 | * PirschBrowserStats is the result export interface for browser statistics.
545 | */
546 | export interface PirschBrowserStats extends PirschMetaStats {
547 | browser: string;
548 | }
549 |
550 | // PirschBrowserVersionStats is the result type for browser version statistics.
551 | export interface PirschBrowserVersionStats extends PirschMetaStats {
552 | browser: string;
553 | browser_version: string;
554 | }
555 |
556 | /**
557 | * PirschOSStats is the result export interface for operating system statistics.
558 | */
559 | export interface PirschOSStats extends PirschMetaStats {
560 | os: string;
561 | }
562 |
563 | // PirschOSVersionStats is the result type for operating system version statistics.
564 | export interface PirschOSVersionStats extends PirschMetaStats {
565 | os: string;
566 | os_version: string;
567 | }
568 |
569 | /**
570 | * PirschReferrerStats is the result export interface for referrer statistics.
571 | */
572 | export interface PirschReferrerStats {
573 | referrer: string;
574 | referrer_name: string;
575 | referrer_icon: string;
576 | visitors: number;
577 | sessions: number;
578 | relative_visitors: number;
579 | bounces: number;
580 | bounce_rate: number;
581 | }
582 |
583 | /**
584 | * PirschPlatformStats is the result export interface for platform statistics.
585 | */
586 | export interface PirschPlatformStats {
587 | platform_desktop: number;
588 | platform_mobile: number;
589 | platform_unknown: number;
590 | relative_platform_desktop: number;
591 | relative_platform_mobile: number;
592 | relative_platform_unknown: number;
593 | }
594 |
595 | /**
596 | * PirschScreenClassStats is the result export interface for screen class statistics.
597 | */
598 | export interface PirschScreenClassStats extends PirschMetaStats {
599 | screen_class: string;
600 | }
601 |
602 | /**
603 | * TagStats is the result export interface for tag statistics.
604 | */
605 | export interface TagStats {
606 | key: string;
607 | value: string;
608 | visitors: number;
609 | views: number;
610 | relative_visitors: number;
611 | relative_views: number;
612 | }
613 |
614 | /**
615 | * PirschKeyword is the result export interface for keyword statistics.
616 | */
617 | export interface PirschKeyword {
618 | keys: string[];
619 | clicks: number;
620 | impressions: number;
621 | ctr: number;
622 | position: number;
623 | }
624 |
625 |
626 | /**
627 | * Funnel is the definition of a funnel.
628 | */
629 | export interface Funnel extends PirschBaseEntity {
630 | domain_id: string
631 | name: string
632 | steps: FunnelStep[]
633 | }
634 |
635 | /**
636 | * FunnelStep is the definition of a funnel step.
637 | */
638 | export interface FunnelStep extends PirschBaseEntity {
639 | funnel_id: string
640 | name: string
641 | step: number
642 | filter: PirschFilter
643 | }
644 |
645 | /**
646 | * FunnelStepData is the result type for a funnel step.
647 | */
648 | export interface FunnelStepData {
649 | step: number
650 | visitors: number
651 | relative_visitors: number
652 | previous_visitors: number
653 | relative_previous_visitors: number
654 | dropped: number
655 | drop_off: number
656 | }
657 |
658 | /**
659 | * FunnelData is the response type for the funnel definition and statistics.
660 | */
661 | export interface FunnelData {
662 | definition: Funnel
663 | data: FunnelStepData[]
664 | }
665 |
666 |
667 | /**
668 | * PirschHttpOptions type
669 | */
670 | export interface PirschHttpOptions {
671 | headers?: Record;
672 | parameters?: object;
673 | }
674 |
675 | /**
676 | * PirschProxyHeader type
677 | */
678 | export type PirschProxyHeader = typeof PIRSCH_PROXY_HEADERS[number];
679 |
680 | /**
681 | * PirschAccessMode type
682 | */
683 | export type PirschAccessMode = "access-token" | "oauth";
684 |
685 | /**
686 | * Protocol type
687 | */
688 | export type Protocol = "http" | "https";
689 |
690 | /**
691 | * Scalar type
692 | */
693 | export type Scalar = string | number | boolean;
694 |
695 | /**
696 | * Optional type
697 | */
698 | export type Optional = T | undefined;
699 |
--------------------------------------------------------------------------------
/src/web-api.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
2 | import { PirschTokenClientConfig, PirschHttpOptions, PirschApiErrorResponse } from "./types";
3 |
4 | import { PirschCoreClient } from "./core";
5 | import { PirschApiError, PirschUnknownApiError } from "./common";
6 |
7 | /**
8 | * Client is used to access the Pirsch API.
9 | */
10 | export class PirschWebApiClient extends PirschCoreClient {
11 | private httpClient: AxiosInstance;
12 |
13 | /**
14 | * The constructor creates a new client.
15 | *
16 | * @param {object} configuration You need to pass in the **Access Token** you have configured on the Pirsch dashboard.
17 | * It's also recommended to set the proper protocol for your website, else it will be set to `https` by default.
18 | * All other configuration parameters can be left to their defaults.
19 | * @param {string} configuration.baseUrl The base URL for the pirsch API
20 | * @param {number} configuration.timeout The default HTTP timeout in milliseconds
21 | * @param {string} configuration.accessToken The access token
22 | *
23 | */
24 | constructor(configuration: PirschTokenClientConfig) {
25 | if ("clientId" in configuration || "clientSecret" in configuration) {
26 | throw new Error("Do not pass OAuth secrets such as 'clientId' or 'clientSecret' to the web client!");
27 | }
28 |
29 | super(configuration);
30 | this.httpClient = axios.create({
31 | baseURL: this.baseUrl,
32 | timeout: this.timeout,
33 | });
34 | }
35 |
36 | protected async get(url: string, options?: PirschHttpOptions): Promise {
37 | const result = await this.httpClient.get(url, this.createOptions({ ...options }));
38 |
39 | return result.data;
40 | }
41 |
42 | protected async post(
43 | url: string,
44 | data: Data,
45 | options?: PirschHttpOptions
46 | ): Promise {
47 | const result = await this.httpClient.post(url, data, this.createOptions({ ...options, data }));
48 |
49 | return result.data;
50 | }
51 |
52 | protected async toApiError(error: unknown): Promise {
53 | if (error instanceof PirschApiError) {
54 | return error;
55 | }
56 |
57 | if (error instanceof AxiosError) {
58 | return new PirschApiError(error.response?.status ?? 400, error.response?.data as PirschApiErrorResponse);
59 | }
60 |
61 | if (typeof error === 'object' && error !== null && 'response' in error && typeof error.response === 'object' && error.response !== null && 'status' in error.response && 'data' in error.response) {
62 | return new PirschApiError(error.response.status as number ?? 400, error.response?.data as PirschApiErrorResponse);
63 | }
64 |
65 | if (error instanceof Error) {
66 | return new PirschUnknownApiError(error.message);
67 | }
68 |
69 | if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') {
70 | return new PirschUnknownApiError(error.message);
71 | }
72 |
73 | return new PirschUnknownApiError(JSON.stringify(error));
74 | }
75 |
76 | private createOptions({ headers, parameters, data }: PirschHttpOptions & { data?: object }): AxiosRequestConfig {
77 | return {
78 | headers,
79 | params: parameters as Record,
80 | data,
81 | };
82 | }
83 | }
84 |
85 | export const Pirsch = PirschWebApiClient;
86 | export const Client = PirschWebApiClient;
87 |
--------------------------------------------------------------------------------
/src/web.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PirschIdentificationCodeClientConfig,
3 | PirschHttpOptions,
4 | PirschApiErrorResponse,
5 | PirschBrowserHit,
6 | Scalar,
7 | } from "./types";
8 |
9 | import { PirschApiError, PirschCommon, PirschUnknownApiError } from "./common";
10 | import { PirschEndpoint, PIRSCH_DEFAULT_BASE_URL, PIRSCH_DEFAULT_TIMEOUT, PIRSCH_URL_LENGTH_LIMIT } from "./constants";
11 | import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
12 |
13 | /**
14 | * Client is used to access the Pirsch API.
15 | */
16 | export class PirschWebClient extends PirschCommon {
17 | private readonly baseUrl: string;
18 | private readonly timeout: number;
19 |
20 | private readonly identificationCode: string;
21 | private readonly hostname?: string;
22 |
23 | private httpClient: AxiosInstance;
24 |
25 | /**
26 | * The constructor creates a new client.
27 | *
28 | * @param {object} configuration You need to pass in the **Identification Code** you have configured on the Pirsch dashboard.
29 | * It's also recommended to set the proper protocol for your website, else it will be set to `https` by default.
30 | * All other configuration parameters can be left to their defaults.
31 | * @param {string} configuration.baseUrl The base URL for the pirsch API
32 | * @param {number} configuration.timeout The default HTTP timeout in milliseconds
33 | * @param {string} configuration.identificationCode The identification code
34 | * @param {string} configuration.hostname The hostname to rewrite the URL. Usually only required for testing
35 | *
36 | */
37 | constructor(configuration: PirschIdentificationCodeClientConfig) {
38 | super();
39 |
40 | if ("clientId" in configuration || "clientSecret" in configuration) {
41 | throw new Error("Do not pass OAuth secrets such as 'clientId' or 'clientSecret' to the web client!");
42 | }
43 |
44 | if ("accessToken" in configuration) {
45 | throw new Error("Do not pass secrets such as 'accessToken' to the web client!");
46 | }
47 |
48 | const {
49 | baseUrl = PIRSCH_DEFAULT_BASE_URL,
50 | timeout = PIRSCH_DEFAULT_TIMEOUT,
51 | identificationCode,
52 | hostname,
53 | } = configuration;
54 |
55 | this.assertIdentificationCodeCredentials({ identificationCode });
56 |
57 | this.baseUrl = baseUrl;
58 | this.timeout = timeout;
59 | this.identificationCode = identificationCode;
60 | this.hostname = hostname;
61 |
62 | this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: this.timeout });
63 | }
64 |
65 | /**
66 | * hit sends a hit to Pirsch.
67 | *
68 | * @param hit optional override data for the request.
69 | */
70 | public async hit(hit?: Partial): Promise {
71 | const data = { ...this.hitFromBrowser(), ...hit };
72 | const parameters = this.browserHitToGetParameters(data);
73 | await this.get(PirschEndpoint.HIT, { parameters });
74 | }
75 |
76 | /**
77 | * event sends an event to Pirsch.
78 | *
79 | * @param name the name for the event
80 | * @param duration optional duration for the event
81 | * @param meta optional object containing metadata (only scalar values, like strings, numbers, and booleans)
82 | * @param hit optional override data for the request
83 | */
84 | public async event(
85 | name: string,
86 | duration = 0,
87 | meta?: Record,
88 | hit?: Partial
89 | ): Promise {
90 | const data = { ...this.hitFromBrowser(), ...hit };
91 | await this.post(
92 | PirschEndpoint.EVENT,
93 | {
94 | identification_code: this.identificationCode,
95 | event_name: name,
96 | event_duration: duration,
97 | event_meta: this.prepareScalarObject(meta),
98 | ...data,
99 | },
100 | { headers: { "Content-Type": "application/json" } }
101 | );
102 | }
103 |
104 | /**
105 | * customHit sends a hit to Pirsch.
106 | *
107 | * @param hit data for the request.
108 | */
109 | public async customHit(hit: PirschBrowserHit): Promise {
110 | const parameters = this.browserHitToGetParameters(hit);
111 | await this.get(PirschEndpoint.HIT, { parameters });
112 | }
113 |
114 | /**
115 | * customEvent sends an event to Pirsch.
116 | *
117 | * @param name the name for the event
118 | * @param duration optional duration for the event
119 | * @param hit data for the request
120 | * @param meta optional object containing metadata (only scalar values, like strings, numbers, and booleans)
121 | */
122 | public async customEvent(
123 | name: string,
124 | duration = 0,
125 | hit: PirschBrowserHit,
126 | meta?: Record
127 | ): Promise {
128 | await this.post(
129 | PirschEndpoint.EVENT,
130 | {
131 | identification_code: this.identificationCode,
132 | event_name: name,
133 | event_duration: duration,
134 | event_meta: this.prepareScalarObject(meta),
135 | ...hit,
136 | },
137 | {
138 | headers: { "Content-Type": "application/json" },
139 | }
140 | );
141 | }
142 |
143 | /**
144 | * hitFromBrowser returns the required data to send a hit to Pirsch.
145 | *
146 | * @returns Hit object containing all necessary fields.
147 | */
148 | public hitFromBrowser(): PirschBrowserHit {
149 | return {
150 | url: this.generateUrl(),
151 | title: document.title,
152 | referrer: document.referrer,
153 | screen_width: screen.width,
154 | screen_height: screen.height,
155 | };
156 | }
157 |
158 | private browserHitToGetParameters(data: PirschBrowserHit) {
159 | const hit: Record = {
160 | nc: Date.now(),
161 | code: this.identificationCode,
162 | url: data.url,
163 | };
164 |
165 | if (data.title) {
166 | hit['t'] = data.title;
167 | }
168 |
169 | if (data.referrer) {
170 | hit['ref'] = data.referrer;
171 | }
172 |
173 | if (data.screen_width) {
174 | hit['w'] = data.screen_width;
175 | }
176 |
177 | if (data.screen_height) {
178 | hit['h'] = data.screen_height;
179 | }
180 |
181 | if (data.tags) {
182 | for (const [key, value] of Object.entries(data.tags)) {
183 | hit[`tag_${key.replaceAll("-", " ")}`] = value || 1;
184 | }
185 | }
186 |
187 | return hit;
188 | }
189 |
190 | private generateUrl() {
191 | const url = this.hostname ? location.href.replace(location.hostname, this.hostname) : location.href;
192 |
193 | return url.slice(0, PIRSCH_URL_LENGTH_LIMIT);
194 | }
195 |
196 | protected async get(url: string, options?: PirschHttpOptions): Promise {
197 | try {
198 | const result = await this.httpClient.get(url, this.createOptions({ ...options }));
199 | return result.data;
200 | } catch (error: unknown) {
201 | const exception = await this.toApiError(error);
202 |
203 | throw exception;
204 | }
205 | }
206 |
207 | protected async post(
208 | url: string,
209 | data: Data,
210 | options?: PirschHttpOptions
211 | ): Promise {
212 | try {
213 | const result = await this.httpClient.post(url, data, this.createOptions(options ?? {}));
214 | return result.data;
215 | } catch (error: unknown) {
216 | const exception = await this.toApiError(error);
217 | throw exception;
218 | }
219 | }
220 |
221 | protected async toApiError(error: unknown): Promise {
222 | if (error instanceof PirschApiError) {
223 | return error;
224 | }
225 |
226 | if (error instanceof AxiosError) {
227 | return new PirschApiError(error.response?.status ?? 400, error.response?.data as PirschApiErrorResponse);
228 | }
229 |
230 | if (typeof error === 'object' && error !== null && 'response' in error && typeof error.response === 'object' && error.response !== null && 'status' in error.response && 'data' in error.response) {
231 | return new PirschApiError(error.response.status as number ?? 400, error.response?.data as PirschApiErrorResponse);
232 | }
233 |
234 | if (error instanceof Error) {
235 | return new PirschUnknownApiError(error.message);
236 | }
237 |
238 | if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') {
239 | return new PirschUnknownApiError(error.message);
240 | }
241 |
242 | return new PirschUnknownApiError(JSON.stringify(error));
243 | }
244 |
245 | private createOptions({ headers, parameters }: PirschHttpOptions): AxiosRequestConfig {
246 | return {
247 | headers,
248 | params: parameters as Record,
249 | };
250 | }
251 | }
252 |
253 | export const Pirsch = PirschWebClient;
254 | export const Client = PirschWebClient;
255 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "allowUnreachableCode": false,
5 | "allowUnusedLabels": false,
6 | "alwaysStrict": true,
7 | "declaration": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "module": "commonjs",
11 | "newLine": "lf",
12 | "noErrorTruncation": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": true,
15 | "noImplicitOverride": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noImplicitUseStrict": false,
19 | "noPropertyAccessFromIndexSignature": true,
20 | "noUncheckedIndexedAccess": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "baseUrl": "src/",
24 | "outDir": "dist/",
25 | "pretty": true,
26 | "lib": ["esnext", "dom", "dom.iterable"],
27 | "skipLibCheck": true,
28 | "strict": true,
29 | "strictFunctionTypes": true,
30 | "strictNullChecks": true,
31 | "target": "es2015"
32 | },
33 | "include": ["src/*.ts"],
34 | "exclude": ["tmp/"]
35 | }
36 |
--------------------------------------------------------------------------------