├── .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 | Version 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 | --------------------------------------------------------------------------------