@vskstudio/takt-astro 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Astro integration for [Takt](https://github.com/vskstudio/takt-core), privacy-fr
11
11
  pnpm add @vskstudio/takt-astro @vskstudio/takt-core
12
12
  ```
13
13
 
14
- Both are peer dependencies: `astro` (`>=4`) and `@vskstudio/takt-core` (`>=0.8.1`).
14
+ Both are peer dependencies: `astro` (`>=4`) and `@vskstudio/takt-core` (`>=0.10.0`).
15
15
 
16
16
  ## Usage
17
17
 
@@ -62,8 +62,11 @@ Both the integration and the component accept the same options, with one excepti
62
62
  | `trackQuery` | `boolean` | `false` | Keep the full query string and hash instead of stripping them. Wins over `queryParams`. |
63
63
  | `queryParams` | `string[]` | – | Allowlist of query params to keep, applied only when `trackQuery` is off. |
64
64
  | `exclude` | `string[]` | – | Path prefixes never tracked, e.g. `['/app', '/account']` (segment-bounded, checked at send time). |
65
- | `scrubUrl` | `(url: string) => string` | – | **Integration only.** Rewrite each URL before it is sent (e.g. strip a fragment or PII). See the note below. |
65
+ | `redactRoutes` | `string[]` | none | Route patterns sent in place of the real path, e.g. `['/verify/[token]']` sends `/verify/[token]` instead of `/verify/abc123`. See [Route redaction](#route-redaction). |
66
+ | `routeTemplates` | `boolean` | `false` | Send every page as its Astro route template (`/blog/[slug]`), read from `Astro.routePattern`. Needs Astro 5 and the route meta, see [Route redaction](#route-redaction). |
67
+ | `scrubUrl` | `(url: string) => string` | – | **Integration only.** Rewrite each URL before it is sent (page, referrer, and the `url` prop of outbound-link and file-download events), e.g. to strip a fragment or PII. See the note below. |
66
68
  | `tagged` | `boolean` | `false` | Auto-track clicks on `[data-takt-event]` elements; `data-takt-prop-*` attributes become event props. |
69
+ | `debug` | `boolean` | `false` | Log each payload to the browser console before sending. |
67
70
 
68
71
  > **`scrubUrl` note.** Unlike the other options, `scrubUrl` is a **function**, so it
69
72
  > is supported **only via the integration**, not the `<Takt />` component. The
@@ -80,9 +83,45 @@ Both the integration and the component accept the same options, with one excepti
80
83
 
81
84
  Astro's client router runs several history operations per navigation (scroll-restoration `replaceState` plus `pushState`), so Takt does **not** rely on core's history patch here — it would over-count. Instead the runtime fires one explicit initial pageview and then one per Astro `astro:after-swap` event (including View Transitions DOM swaps and back/forward), so each navigation is counted exactly once. On a plain MPA (no client router) the script re-runs per page load, which fires the initial pageview each time. The runtime is SSR/prerender safe — it only runs in the browser.
82
85
 
86
+ ## Route redaction
87
+
88
+ Query strings are stripped by default, but path segments are sent as they are, so `/verify/abc123` leaks the token. List the sensitive routes in `redactRoutes` and every matching path is sent as its pattern, while the other pages keep their real path:
89
+
90
+ ```js
91
+ takt({ redactRoutes: ['/verify/[token]', '/invoices/[id].pdf'] })
92
+ ```
93
+
94
+ The rule covers the page URL, same-origin referrers, outbound and download link destinations, and 404 paths. Patterns use Astro's syntax (`[param]`, `[...rest]`) and also accept `:param` and `*`.
95
+
96
+ To send every page as its route template instead (`/blog/hello` becomes `/blog/[slug]`), turn on `routeTemplates`. Astro knows the template on the server (`Astro.routePattern`), so it travels in a `<meta name="takt:route">` tag that the runtime reads at each pageview, including after a ClientRouter navigation. With the integration, render that tag with `<TaktRoute />` in the `<head>` of your layout:
97
+
98
+ ```js
99
+ // astro.config.mjs
100
+ export default defineConfig({
101
+ integrations: [takt({ routeTemplates: true })],
102
+ })
103
+ ```
104
+
105
+ ```astro
106
+ ---
107
+ import TaktRoute from '@vskstudio/takt-astro/TaktRoute.astro'
108
+ ---
109
+ <head>
110
+ <TaktRoute />
111
+ </head>
112
+ ```
113
+
114
+ The `<Takt />` component renders the tag itself when `routeTemplates` is set:
115
+
116
+ ```astro
117
+ <Takt domain="example.com" routeTemplates />
118
+ ```
119
+
120
+ `Astro.routePattern` exists from Astro 5. On Astro 4 no tag is rendered, so `redactRoutes` still applies and the real path is sent otherwise. This mode merges every article of a public blog into one row, so it suits private areas better; `redactRoutes` is usually the right tool for a public site.
121
+
83
122
  ## Custom events
84
123
 
85
- `track`, `pageview`, `optOut` and `optIn` are re-exported from core for convenience. They act on core's default instance, which only exists in the browser once the injected runtime has booted — so call them from a client-side `<script>`, not from `.astro` frontmatter (where they are silent no-ops):
124
+ `track`, `pageview`, `optOut`, `optIn` and `isOptedOut` are re-exported from core for convenience. `track` and `pageview` act on core's default instance, which only exists in the browser once the injected runtime has booted, so call them from a client-side `<script>`, not from `.astro` frontmatter (where they are silent no-ops):
86
125
 
87
126
  ```astro
88
127
  <button id="signup">Sign up</button>
@@ -94,6 +133,26 @@ Astro's client router runs several history operations per navigation (scroll-res
94
133
  </script>
95
134
  ```
96
135
 
136
+ ## Consent
137
+
138
+ `optOut`, `optIn` and `isOptedOut` do not wait for the runtime to boot: they read and write the stored choice directly. A consent banner can therefore run before the injected runtime, and the instance it boots later honours the choice. Call them from a client-side `<script>`, since the choice lives in the browser.
139
+
140
+ ```astro
141
+ <button id="analytics-toggle"></button>
142
+ <script>
143
+ import { isOptedOut, optIn, optOut } from '@vskstudio/takt-astro'
144
+ const button = document.getElementById('analytics-toggle')
145
+ const render = () => {
146
+ if (button) button.textContent = isOptedOut() ? 'Enable analytics' : 'Disable analytics'
147
+ }
148
+ button?.addEventListener('click', () => {
149
+ isOptedOut() ? optIn() : optOut()
150
+ render()
151
+ })
152
+ render()
153
+ </script>
154
+ ```
155
+
97
156
  ## Widgets
98
157
 
99
158
  Thin wrappers around Takt's server-rendered badge SVG and embed iframe.
@@ -137,10 +196,10 @@ From `@vskstudio/takt-astro`:
137
196
 
138
197
  - `default` / `takt` — the integration factory (same function under both names)
139
198
  - `resolveOptions`, `assertNoScrubUrl`, `buildRuntime` — the internals the `.astro` components build on, exported so you can drive the runtime yourself
140
- - Re-exported from core: `track`, `pageview`, `optOut`, `optIn`, `createStats`, `PublicApiError`, `badgeUrl`, `embedUrl`
199
+ - Re-exported from core: `track`, `pageview`, `optOut`, `optIn`, `isOptedOut`, `createStats`, `PublicApiError`, `badgeUrl`, `embedUrl`
141
200
  - Types: `TaktOptions`, plus `InitOptions`, `TrackOptions`, `BadgeOptions`, `EmbedOptions`, `BadgeVariant`, `BadgeGlyph`, `EmbedTheme`, `WidgetLang`, `StatsClient`, `StatsClientOptions`, `StatsParams`, `StatsPeriod`, `StatsDimension`, `StatsMetrics`, `StatsSummary`, `StatsPoint`, `StatsTimeseries`, `StatsBreakdownRow`, `StatsBreakdown`, `StatsRealtime` re-exported from core
142
201
 
143
- Component subpaths: `@vskstudio/takt-astro/Takt.astro`, `/Badge.astro`, `/Embed.astro`.
202
+ Component subpaths: `@vskstudio/takt-astro/Takt.astro`, `/TaktRoute.astro`, `/Badge.astro`, `/Embed.astro`.
144
203
 
145
204
  ## License
146
205
 
package/Takt.astro CHANGED
@@ -17,7 +17,10 @@ const config = JSON.stringify(resolveOptions(Astro.props))
17
17
  .replace(/</g, '\\u003c')
18
18
  .replace(/>/g, '\\u003e')
19
19
  .replace(/&/g, '\\u0026')
20
+
21
+ const route = Astro.props.routeTemplates ? Astro.routePattern : undefined
20
22
  ---
23
+ {route && <meta name="takt:route" content={route} />}
21
24
  <!-- Pick ONE install path — this component OR the integration — not both. -->
22
25
  <script type="application/json" data-takt-config set:html={config}></script>
23
26
  <!-- A bundled (non-inline) module so Astro resolves the bare core import; an
@@ -29,9 +32,12 @@ const config = JSON.stringify(resolveOptions(Astro.props))
29
32
  if (el && !w.__takt) {
30
33
  const o = JSON.parse(el.textContent || '{}')
31
34
  w.__takt = true
35
+ const routeTemplate = o.routeTemplates
36
+ ? () => document.querySelector('meta[name="takt:route"]')?.getAttribute('content') ?? null
37
+ : undefined
32
38
  // auto forced off: Astro's ClientRouter runs several history ops per nav, so
33
39
  // core's history patch would over-count. Drive pageviews from after-swap.
34
- init({ ...o, auto: false })
40
+ init({ ...o, auto: false, routeTemplate })
35
41
  pageview()
36
42
  if (o.auto !== false) document.addEventListener('astro:after-swap', () => pageview())
37
43
  }
@@ -0,0 +1,4 @@
1
+ ---
2
+ const route = Astro.routePattern
3
+ ---
4
+ {route && <meta name="takt:route" content={route} />}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroIntegration } from 'astro';
2
2
  import { InitOptions } from '@vskstudio/takt-core';
3
- export { BadgeGlyph, BadgeOptions, BadgeVariant, EmbedOptions, EmbedTheme, InitOptions, PublicApiError, StatsBreakdown, StatsBreakdownRow, StatsClient, StatsClientOptions, StatsDimension, StatsMetrics, StatsParams, StatsPeriod, StatsPoint, StatsRealtime, StatsSummary, StatsTimeseries, TrackOptions, WidgetLang, badgeUrl, createStats, embedUrl, optIn, optOut, pageview, track } from '@vskstudio/takt-core';
3
+ export { BadgeGlyph, BadgeOptions, BadgeVariant, EmbedOptions, EmbedTheme, InitOptions, PublicApiError, StatsBreakdown, StatsBreakdownRow, StatsClient, StatsClientOptions, StatsDimension, StatsMetrics, StatsParams, StatsPeriod, StatsPoint, StatsRealtime, StatsSummary, StatsTimeseries, TrackOptions, WidgetLang, badgeUrl, createStats, embedUrl, isOptedOut, optIn, optOut, pageview, track } from '@vskstudio/takt-core';
4
4
 
5
5
  /**
6
6
  * Options for the Takt integration and `<Takt />` component. Mirrors core's
@@ -41,6 +41,8 @@ interface TaktOptions {
41
41
  queryParams?: string[];
42
42
  /** Path prefixes never tracked, e.g. `['/app','/account']`. Segment-bounded: `'/app'` matches `'/app'` and `'/app/…'` but not `'/application'`. */
43
43
  exclude?: string[];
44
+ redactRoutes?: string[];
45
+ routeTemplates?: boolean;
44
46
  /**
45
47
  * Rewrite each URL before it is sent (e.g. strip a fragment or PII).
46
48
  * MUST be a self-contained function: it is stringified at build time and
@@ -54,6 +56,7 @@ interface TaktOptions {
54
56
  scrubUrl?: (url: string) => string;
55
57
  /** Auto-track elements marked with `data-takt-event`. */
56
58
  tagged?: boolean;
59
+ debug?: boolean;
57
60
  }
58
61
  /**
59
62
  * Guard the `<Takt />` component path against `scrubUrl`. The component
package/dist/index.js CHANGED
@@ -22,15 +22,23 @@ function resolveOptions(options = {}) {
22
22
  trackQuery: options.trackQuery,
23
23
  queryParams: options.queryParams,
24
24
  exclude: options.exclude,
25
- tagged: options.tagged
25
+ redactRoutes: options.redactRoutes,
26
+ routeTemplates: options.routeTemplates,
27
+ tagged: options.tagged,
28
+ debug: options.debug
26
29
  };
27
30
  }
28
31
 
29
32
  // src/runtime.ts
33
+ var ROUTE_TEMPLATE_RESOLVER = `() => document.querySelector('meta[name="takt:route"]')?.getAttribute('content') ?? null`;
30
34
  function buildRuntime(options, scrubUrl) {
31
35
  const spa = options.auto !== false;
32
36
  const json = JSON.stringify({ ...options, auto: false }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
33
- const config = scrubUrl ? `Object.assign(${json}, { scrubUrl: ${scrubUrl.toString()} })` : json;
37
+ const functions = [
38
+ ...scrubUrl ? [`scrubUrl: ${scrubUrl.toString()}`] : [],
39
+ ...options.routeTemplates ? [`routeTemplate: ${ROUTE_TEMPLATE_RESOLVER}`] : []
40
+ ];
41
+ const config = functions.length ? `Object.assign(${json}, { ${functions.join(", ")} })` : json;
34
42
  return [
35
43
  `import { init, pageview } from '@vskstudio/takt-core';`,
36
44
  // SSR/prerender guard: this module is evaluated under Node when Astro
@@ -65,7 +73,7 @@ function takt(options = {}) {
65
73
  }
66
74
 
67
75
  // src/index.ts
68
- import { track, pageview, optOut, optIn } from "@vskstudio/takt-core";
76
+ import { track, pageview, optOut, optIn, isOptedOut } from "@vskstudio/takt-core";
69
77
  import { createStats, PublicApiError, badgeUrl, embedUrl } from "@vskstudio/takt-core";
70
78
  export {
71
79
  PublicApiError,
@@ -75,6 +83,7 @@ export {
75
83
  createStats,
76
84
  takt as default,
77
85
  embedUrl,
86
+ isOptedOut,
78
87
  optIn,
79
88
  optOut,
80
89
  pageview,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vskstudio/takt-astro",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Astro integration for Takt privacy-friendly analytics",
5
5
  "license": "MIT",
6
6
  "author": "VSK Studio",
@@ -32,6 +32,7 @@
32
32
  "default": "./dist/index.js"
33
33
  },
34
34
  "./Takt.astro": "./Takt.astro",
35
+ "./TaktRoute.astro": "./TaktRoute.astro",
35
36
  "./Badge.astro": "./Badge.astro",
36
37
  "./Embed.astro": "./Embed.astro",
37
38
  "./package.json": "./package.json"
@@ -39,31 +40,22 @@
39
40
  "files": [
40
41
  "dist",
41
42
  "Takt.astro",
43
+ "TaktRoute.astro",
42
44
  "Badge.astro",
43
45
  "Embed.astro"
44
46
  ],
45
47
  "engines": {
46
48
  "node": ">=18"
47
49
  },
48
- "scripts": {
49
- "build": "tsup",
50
- "typecheck": "tsc --noEmit",
51
- "lint": "eslint .",
52
- "test": "vitest run",
53
- "e2e": "playwright test",
54
- "size": "size-limit",
55
- "prepublishOnly": "pnpm build",
56
- "release": "pnpm build && changeset publish"
57
- },
58
50
  "peerDependencies": {
59
- "@vskstudio/takt-core": ">=0.8.1",
51
+ "@vskstudio/takt-core": ">=0.10.0",
60
52
  "astro": ">=4"
61
53
  },
62
54
  "devDependencies": {
63
55
  "@changesets/cli": "^2.31.0",
64
56
  "@playwright/test": "^1.50.0",
65
57
  "@size-limit/preset-small-lib": "^11.1.6",
66
- "@vskstudio/takt-core": "^0.8.1",
58
+ "@vskstudio/takt-core": "^0.10.0",
67
59
  "astro": "^5.0.0",
68
60
  "eslint": "^9.0.0",
69
61
  "size-limit": "^11.1.6",
@@ -72,5 +64,13 @@
72
64
  "typescript-eslint": "^8.0.0",
73
65
  "vitest": "^2.1.0"
74
66
  },
75
- "packageManager": "pnpm@9.0.0"
76
- }
67
+ "scripts": {
68
+ "build": "tsup",
69
+ "typecheck": "tsc --noEmit",
70
+ "lint": "eslint .",
71
+ "test": "vitest run",
72
+ "e2e": "playwright test",
73
+ "size": "size-limit",
74
+ "release": "pnpm build && changeset publish"
75
+ }
76
+ }