├── .gitignore ├── .gitattributes ├── packages └── astro-umami │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ └── src │ └── index.ts ├── readme.md ├── .npmrc ├── playground ├── src │ ├── env.d.ts │ └── pages │ │ └── index.astro ├── .gitignore ├── tsconfig.json ├── package.json ├── playwright.config.ts ├── astro.config.ts └── tests │ └── index.spec.ts ├── pnpm-workspace.yaml ├── .editorconfig ├── .github ├── workflows │ ├── release.yml │ └── test.yml └── dependabot.yml ├── package.json ├── license └── eslint.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /packages/astro-umami/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | packages/astro-umami/readme.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check = true 2 | save-exact = true 3 | -------------------------------------------------------------------------------- /playground/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - playground 4 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | .astro 2 | /node_modules/ 3 | /playwright-report/ 4 | /test-results/ 5 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "module": "ESNext" 7 | }, 8 | "exclude": [ 9 | "dist" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /playground/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to Astro! 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md,*.mdx] 13 | max_line_length = 80 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /packages/astro-umami/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "lib": [ 6 | "es2022" 7 | ], 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "outDir": "dist" 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | 22 | - run: npx changelogithub 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 5 8 | labels: 9 | - dependencies 10 | groups: 11 | # Group ESLint dependencies together 12 | eslint: 13 | patterns: 14 | - "@eslint/js" 15 | - "eslint" 16 | # Group Playwright dependencies together 17 | playwright: 18 | patterns: 19 | - "@playwright/test" 20 | - "playwright" 21 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "build": "astro check && astro build", 8 | "dev": "astro dev", 9 | "preview": "astro preview", 10 | "start": "astro dev", 11 | "test": "playwright test" 12 | }, 13 | "dependencies": { 14 | "@yeskunall/astro-umami": "workspace:*", 15 | "astro": "5.16.6", 16 | "astro-integration-kit": "0.19.1" 17 | }, 18 | "devDependencies": { 19 | "@astrojs/check": "0.9.6", 20 | "@playwright/test": "1.57.0", 21 | "playwright": "1.57.0", 22 | "typescript": "5.9.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /playground/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | import { defineConfig, devices } from "@playwright/test"; 3 | 4 | export default defineConfig({ 5 | testDir: "tests", 6 | fullyParallel: true, 7 | projects: [ 8 | { 9 | name: "chromium", 10 | use: { ...devices["Desktop Chrome"], channel: "chromium" }, 11 | }, 12 | ], 13 | reporter: "html", 14 | retries: process.env.CI ? 2 : 0, 15 | use: { 16 | baseURL: "http://localhost:4321", 17 | trace: "on-first-retry", 18 | }, 19 | webServer: { 20 | command: "pnpm dev", 21 | url: "http://localhost:4321", 22 | reuseExistingServer: !process.env.CI, 23 | }, 24 | workers: process.env.CI ? 1 : undefined, 25 | }); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "packageManager": "pnpm@9.15.6", 5 | "license": "MIT", 6 | "engines": { 7 | "node": "^18.20.8 || ^20.3.0 || >=22" 8 | }, 9 | "scripts": { 10 | "build": "pnpm --filter=\"./packages/*\" --recursive run build", 11 | "dev": "pnpm --filter=\"./packages/*\" --recursive run dev", 12 | "lint": "eslint --max-warnings 0 .", 13 | "prepublishOnly": "pnpm run build", 14 | "test": "pnpm --filter=\"playground\" --recursive run test" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "9.39.2", 18 | "@stylistic/eslint-plugin": "5.6.1", 19 | "eslint": "9.39.2", 20 | "jiti": "2.6.1", 21 | "typescript-eslint": "8.50.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/astro.config.ts: -------------------------------------------------------------------------------- 1 | import { createResolver } from "astro-integration-kit"; 2 | import { hmrIntegration } from "astro-integration-kit/dev"; 3 | import { defineConfig } from "astro/config"; 4 | 5 | const { default: packageName } = await import("@yeskunall/astro-umami"); 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | integrations: [ 10 | packageName({ 11 | autotrack: false, 12 | beforeSendHandler: "beforeSendHandler", 13 | doNotTrack: true, 14 | excludeHash: true, 15 | excludeSearch: true, 16 | id: "94db1cb1-74f4-4a40-ad6c-962362670409", 17 | domains: ["example.com", "com.example"], 18 | hostUrl: "https://analytics.eu.umami.is", 19 | tag: "test-tag", 20 | withPartytown: true, 21 | }), 22 | hmrIntegration({ 23 | directory: createResolver(import.meta.url).resolve( 24 | "../packages/astro-umami", 25 | ), 26 | }), 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: E2E tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | actions: read 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | name: Node.js ${{ matrix.node-version }} 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: 22 | - 18 23 | - 20 24 | - 22 25 | - 24 26 | timeout-minutes: 30 27 | defaults: 28 | run: 29 | working-directory: playground 30 | steps: 31 | - uses: actions/checkout@v5 32 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda 33 | - uses: actions/setup-node@v5 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: pnpm 37 | - run: | 38 | pnpm install --frozen-lockfile 39 | pnpm exec playwright install --with-deps chromium --no-shell 40 | - run: pnpm build 41 | working-directory: packages/astro-umami 42 | - run: pnpm test 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Kunall Banerjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import js from "@eslint/js"; 3 | import stylistic from "@stylistic/eslint-plugin"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | import type { Linter } from "eslint"; 7 | 8 | const stylisticConfig = stylistic.configs.customize({ 9 | indent: 2, 10 | quotes: "double", 11 | semi: true, 12 | }); 13 | 14 | const stylisticRulesAsWarnings: Record 15 | = Object.entries(stylisticConfig.rules || {}).reduce( 16 | (acc, [ruleName, ruleConfig]) => { 17 | if (Array.isArray(ruleConfig)) { 18 | // Rule has configuration options: ["error", { options }] -> ["warn", { options }] 19 | acc[ruleName] = ["warn", ...ruleConfig.slice(1)]; 20 | } 21 | else { 22 | // Rule is just a severity level: "error" -> "warn" 23 | acc[ruleName] = "warn"; 24 | } 25 | 26 | return acc; 27 | }, 28 | {} as Record, 29 | ); 30 | 31 | export default defineConfig( 32 | { ignores: ["**/dist/**"] }, 33 | { 34 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 35 | files: ["**/*.{ts,tsx}"], 36 | languageOptions: { 37 | ecmaVersion: "latest", 38 | }, 39 | plugins: { 40 | "@stylistic": stylistic, 41 | }, 42 | rules: { 43 | ...stylisticRulesAsWarnings, 44 | }, 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /packages/astro-umami/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yeskunall/astro-umami", 3 | "type": "module", 4 | "version": "0.0.7", 5 | "description": "Add Umami Analytics to your Astro website", 6 | "author": { 7 | "name": "Kunall Banerjee", 8 | "email": "hey@kunall.dev", 9 | "url": "https://kunall.dev" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/yeskunall/astro-umami#readme", 13 | "repository": "yeskunall/astro-umami", 14 | "bugs": { 15 | "url": "https://github.com/yeskunall/astro-umami#issues" 16 | }, 17 | "keywords": [ 18 | "alternative", 19 | "analytics", 20 | "astro", 21 | "astro-integration", 22 | "ccpa", 23 | "gdpr", 24 | "pecr", 25 | "privacy", 26 | "replacement", 27 | "umami", 28 | "umami.is", 29 | "umami-analytics", 30 | "withastro" 31 | ], 32 | "exports": { 33 | ".": { 34 | "types": "./dist/index.d.ts", 35 | "default": "./dist/index.js" 36 | } 37 | }, 38 | "files": [ 39 | "dist" 40 | ], 41 | "scripts": { 42 | "build": "tsup src/index.ts --no-config --external astro-integration-kit --clean --dts --format esm --minify --target node18", 43 | "dev": "tsup src/index.ts --no-config --format esm --clean --watch --target node18" 44 | }, 45 | "peerDependencies": { 46 | "astro": "^3.0.0 || ^4.0.0 || ^5.0.0" 47 | }, 48 | "devDependencies": { 49 | "tsup": "8.5.1", 50 | "typescript": "5.9.3" 51 | }, 52 | "publishConfig": { 53 | "access": "public" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/astro-umami/readme.md: -------------------------------------------------------------------------------- 1 | # astro-umami 2 | 3 | > An [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) to add [Umami Analytics](https://umami.is/) to your website. 4 | 5 | [![E2E tests](https://github.com/yeskunall/astro-umami/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/yeskunall/astro-umami/actions/workflows/test.yml) 6 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yeskunall/astro-umami/blob/main/license) 7 | [![npm version](https://badge.fury.io/js/@yeskunall%2Fastro-umami.svg)](https://badge.fury.io/js/@yeskunall%2Fastro-umami) 8 | ![monthly downloads](https://img.shields.io/npm/dm/@yeskunall/astro-umami.svg?logo=npm) 9 | 10 | ## Highlights 11 | 12 | - Automatically detects if you’re using [View Transitions](https://docs.astro.build/en/guides/view-transitions/) and adds a [`data-astro-rerun`](https://docs.astro.build/en/guides/view-transitions/#data-astro-rerun) attribute 13 | - Disables events and pageviews during development 14 | - Supports all [configuration](https://umami.is/docs/tracker-configuration) options, unlike [`astro-analytics`](https://github.com/Destiner/astro-analytics) 15 | - Provides inline documentation for all configuration options (thanks to [TypeScript](https://github.com/yeskunall/astro-umami/blob/main/packages/astro-umami/src/index.ts#L7)) 16 | - (_Optionally_) Serve the tracking script using [Partytown](https://partytown.qwik.dev/) (v0.0.5 onwards) 17 | - **Actively maintained** 18 | - **Zero dependencies** 19 | 20 | ## Usage 21 | 22 | ### Install 23 | 24 | Run the following from your project directory and follow the prompts: 25 | 26 | ```sh 27 | pnpm astro add @yeskunall/astro-umami@0.0.7 28 | ``` 29 | 30 | This will install and make the appropriate changes to your Astro config automatically. 31 | 32 | ### Manual install 33 | 34 | 1. Install the required dependencies 35 | 36 | ```sh 37 | pnpm add @yeskunall/astro-umami@0.0.7 38 | ``` 39 | 40 | 2. Add the integration to your Astro config: 41 | 42 | ```diff 43 | + import umami from "@yeskunall/astro-umami"; 44 | 45 | export default defineConfig({ 46 | integrations: [ 47 | + umami({ id: "94db1cb1-74f4-4a40-ad6c-962362670409" }), 48 | ], 49 | }); 50 | ``` 51 | 52 | ###### For all configurable options, see the [interface](https://github.com/yeskunall/astro-umami/blob/main/packages/astro-umami/src/index.ts#L7). 53 | -------------------------------------------------------------------------------- /packages/astro-umami/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AstroIntegration } from "astro"; 2 | 3 | type OptionalExceptFor = { 4 | [P in keyof T]: P extends K ? T[P] : T[P] | undefined; 5 | } & Pick; 6 | 7 | interface UmamiOptions { 8 | /** 9 | * Umami tracks all events and pageviews for you automatically. Override this behavior if you plan on using [tracker functions](https://umami.is/docs/tracker-functions). 10 | * 11 | @default true 12 | */ 13 | autotrack?: boolean; 14 | /** 15 | * Specify a [function](https://umami.is/docs/tracker-configuration#data-before-send) that will be called before data is sent. 16 | */ 17 | beforeSendHandler?: string; 18 | /** 19 | * If you want the tracker to only run on specific domains, add them to this list. 20 | * 21 | * @example ["mywebsite.com", "mywebsite2.com"] 22 | */ 23 | domains?: string[]; 24 | /** 25 | * Respect a visitor’s [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) browser setting. 26 | */ 27 | doNotTrack?: boolean; 28 | /** 29 | * 30 | * The endpoint where your Umami instance is located. 31 | * 32 | * @default https://cloud.umami.is 33 | * @example https://umami-on.fly.dev 34 | */ 35 | endpointUrl?: string; 36 | /** 37 | * Set this if you don’t want to collect the hash value from the URL. 38 | */ 39 | excludeHash?: boolean; 40 | /** 41 | * Set this if you don’t want to collect search parameters from the URL. 42 | */ 43 | excludeSearch?: boolean; 44 | /** 45 | * Override the location where your analytics data is sent. 46 | */ 47 | hostUrl?: string; 48 | /** 49 | * The unique ID of your [website](https://umami.is/docs/add-a-website). 50 | */ 51 | id: string; 52 | /** 53 | * Collect events under a specific tag. These events can be filtered in the dashboard by the specific tag. 54 | */ 55 | tag?: string; 56 | /** 57 | * Assign a custom name to the tracker script. 58 | * 59 | * @default script.js 60 | * @see [https://umami.is/docs/environment-variables](https://umami.is/docs/environment-variables) 61 | */ 62 | trackerScriptName?: string; 63 | } 64 | 65 | interface Options extends UmamiOptions { 66 | /** 67 | * Serve the tracking script using [Partytown](https://partytown.qwik.dev/). 68 | * 69 | * @see [https://docs.astro.build/en/guides/integrations-guide/partytown/](https://docs.astro.build/en/guides/integrations-guide/partytown/) 70 | */ 71 | withPartytown?: boolean; 72 | } 73 | 74 | async function getInjectableWebAnalyticsContent({ 75 | mode, 76 | options, 77 | }: { 78 | mode: "development" | "production"; 79 | options: OptionalExceptFor; 80 | }): Promise { 81 | const { 82 | autotrack = true, 83 | beforeSendHandler, 84 | domains = [], 85 | doNotTrack = false, 86 | endpointUrl = "https://cloud.umami.is", 87 | excludeHash = false, 88 | excludeSearch = false, 89 | hostUrl = "https://cloud.umami.is", 90 | id, 91 | tag, 92 | trackerScriptName = "script.js", 93 | withPartytown = false, 94 | } = options; 95 | 96 | const hostname = new URL(endpointUrl).hostname; 97 | const configAsString = [ 98 | !autotrack ? `script.setAttribute("data-auto-track", "${autotrack}")` : "", 99 | beforeSendHandler ? `script.setAttribute("data-before-send", "${beforeSendHandler}")` : "", 100 | domains.length > 0 101 | ? `script.setAttribute("data-domains", "${domains.join(",")}")` 102 | : "", 103 | doNotTrack ? `script.setAttribute("data-do-not-track", "${doNotTrack}")` : "", 104 | excludeHash ? `script.setAttribute("data-exclude-hash", "${excludeHash}")` : "", 105 | excludeSearch ? `script.setAttribute("data-exclude-search", "${excludeSearch}")` : "", 106 | hostUrl !== "https://cloud.umami.is" 107 | ? `script.setAttribute("data-host-url", "${hostUrl}")` 108 | : "", 109 | tag ? `script.setAttribute("data-tag", "${tag}")` : "", 110 | withPartytown ? `script.setAttribute("type", "text/partytown")` : "", 111 | ] 112 | .filter(Boolean) 113 | .join(";\n"); 114 | 115 | const commonScript = ` 116 | var script = document.createElement("script"); 117 | var viewTransitionsEnabled = document.querySelector("meta[name='astro-view-transitions-enabled']")?.content; 118 | 119 | script.setAttribute("src", "https://${hostname}/${trackerScriptName}"); 120 | script.setAttribute("defer", true); 121 | script.setAttribute("data-website-id", "${id}"); 122 | ${configAsString}; 123 | 124 | if (!!viewTransitionsEnabled) { 125 | script.setAttribute("data-astro-rerun", true); 126 | } 127 | 128 | var head = document.querySelector("head"); 129 | head.appendChild(script); 130 | `; 131 | 132 | if (mode === "development") { 133 | return ` 134 | (function () { 135 | localStorage.setItem("umami.disabled", "1"); 136 | 137 | ${commonScript} 138 | })() 139 | `; 140 | } 141 | 142 | return ` 143 | (function () { 144 | ${commonScript} 145 | })() 146 | `; 147 | } 148 | 149 | export default function umamiIntegration(options: OptionalExceptFor): AstroIntegration { 150 | return { 151 | name: "@yeskunall/astro-umami", 152 | hooks: { 153 | "astro:config:setup": async ({ command, injectScript }) => { 154 | const script = await getInjectableWebAnalyticsContent({ 155 | mode: command === "dev" ? "development" : "production", 156 | options, 157 | }); 158 | 159 | injectScript("head-inline", script); 160 | }, 161 | }, 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /playground/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | // NOTE: All tests query using the `website-id` data attribute because this is 4 | // the only required property as per the docs. In other words, if the script 5 | // exists on the page, then it should be safe to query based on this selector. 6 | 7 | test("`autotrack` disabled", async ({ page }) => { 8 | await page.goto("/"); 9 | 10 | const src = await page.evaluate( 11 | async () => { 12 | const autotrack = document 13 | .querySelector(("script[data-website-id]")) 14 | ?.attributes 15 | .getNamedItem("data-auto-track") 16 | ?.textContent; 17 | 18 | return autotrack; 19 | }, 20 | ); 21 | 22 | expect(src).toBe("false"); 23 | }); 24 | 25 | test("`beforeSendHandler` is valid", async ({ page }) => { 26 | await page.goto("/"); 27 | 28 | const src = await page.evaluate( 29 | async () => { 30 | const beforeSendHandler = document 31 | .querySelector(("script[data-website-id]")) 32 | ?.attributes 33 | .getNamedItem("data-before-send") 34 | ?.textContent; 35 | 36 | return beforeSendHandler; 37 | }, 38 | ); 39 | 40 | expect(src).toBe("beforeSendHandler"); 41 | }); 42 | 43 | test("`doNotTrack` enabled", async ({ page }) => { 44 | await page.goto("/"); 45 | 46 | const src = await page.evaluate( 47 | async () => { 48 | const dnt = document 49 | .querySelector(("script[data-website-id]")) 50 | ?.attributes 51 | .getNamedItem("data-do-not-track") 52 | ?.textContent; 53 | 54 | return dnt; 55 | }, 56 | ); 57 | 58 | expect(src).toBe("true"); 59 | }); 60 | 61 | test("`excludeHash` enabled", async ({ page }) => { 62 | await page.goto("/"); 63 | 64 | const src = await page.evaluate( 65 | async () => { 66 | const excludeHash = document 67 | .querySelector(("script[data-website-id]")) 68 | ?.attributes 69 | .getNamedItem("data-exclude-hash") 70 | ?.textContent; 71 | 72 | return excludeHash; 73 | }, 74 | ); 75 | 76 | expect(src).toBe("true"); 77 | }); 78 | 79 | test("`excludeSearch` enabled", async ({ page }) => { 80 | await page.goto("/"); 81 | 82 | const src = await page.evaluate( 83 | async () => { 84 | const excludeSearch = document 85 | .querySelector(("script[data-website-id]")) 86 | ?.attributes 87 | .getNamedItem("data-exclude-search") 88 | ?.textContent; 89 | 90 | return excludeSearch; 91 | }, 92 | ); 93 | 94 | expect(src).toBe("true"); 95 | }); 96 | 97 | test("`src` matches", async ({ page }) => { 98 | await page.goto("/"); 99 | 100 | const src = await page.evaluate( 101 | async () => { 102 | const src = document 103 | .querySelector(("script[data-website-id]")) 104 | ?.attributes 105 | .getNamedItem("src") 106 | ?.textContent; 107 | 108 | return src; 109 | }, 110 | ); 111 | 112 | expect(src).toBe("https://cloud.umami.is/script.js"); 113 | }); 114 | 115 | test("`id` matches", async ({ page }) => { 116 | await page.goto("/"); 117 | 118 | const id = await page.evaluate( 119 | async () => { 120 | const id = document 121 | .querySelector(("script[data-website-id]")) 122 | ?.attributes 123 | .getNamedItem("data-website-id") 124 | ?.textContent; 125 | 126 | return id; 127 | }, 128 | ); 129 | 130 | expect(id).toBe("94db1cb1-74f4-4a40-ad6c-962362670409"); 131 | }); 132 | 133 | test("`domains` match", async ({ page }) => { 134 | await page.goto("/"); 135 | 136 | const domains = await page.evaluate( 137 | async () => { 138 | const domains = document 139 | .querySelector(("script[data-website-id]")) 140 | ?.attributes 141 | .getNamedItem("data-domains") 142 | ?.textContent; 143 | 144 | return domains; 145 | }, 146 | ); 147 | 148 | expect(domains).toBe("example.com,com.example"); 149 | }); 150 | 151 | test("`hostUrl` matches", async ({ page }) => { 152 | await page.goto("/"); 153 | 154 | const hostUrl = await page.evaluate( 155 | async () => { 156 | const hostUrl = document 157 | .querySelector(("script[data-website-id]")) 158 | ?.attributes 159 | .getNamedItem("data-host-url") 160 | ?.textContent; 161 | 162 | return hostUrl; 163 | }, 164 | ); 165 | 166 | expect(hostUrl).toBe("https://analytics.eu.umami.is"); 167 | }); 168 | 169 | test("`tag` matches", async ({ page }) => { 170 | await page.goto("/"); 171 | 172 | const src = await page.evaluate( 173 | async () => { 174 | const beforeSendHandler = document 175 | .querySelector(("script[data-website-id]")) 176 | ?.attributes 177 | .getNamedItem("data-tag") 178 | ?.textContent; 179 | 180 | return beforeSendHandler; 181 | }, 182 | ); 183 | 184 | expect(src).toBe("test-tag"); 185 | }); 186 | 187 | test("`withPartytown` enabled", async ({ page }) => { 188 | await page.goto("/"); 189 | 190 | const hostUrl = await page.evaluate( 191 | async () => { 192 | const hostUrl = document 193 | .querySelector(("script[data-website-id]")) 194 | ?.attributes 195 | .getNamedItem("type") 196 | ?.textContent; 197 | 198 | return hostUrl; 199 | }, 200 | ); 201 | 202 | expect(hostUrl).toBe("text/partytown"); 203 | }); 204 | --------------------------------------------------------------------------------