diff --git a/package.json b/package.json
index c47da4d..fa6b414 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "ttrpg-player-landing",
"private": true,
- "version": "1.0.1",
+ "version": "1.0.2",
"description": "Marketing landing for TTRPG Player (НРИ Плеер)",
"type": "module",
"scripts": {
diff --git a/src/components/sections/Platforms.tsx b/src/components/sections/Platforms.tsx
index 3f39924..bfd73a1 100644
--- a/src/components/sections/Platforms.tsx
+++ b/src/components/sections/Platforms.tsx
@@ -7,6 +7,7 @@ import {
resolveDownloadHref,
resolvePlatformDownloadHref,
} from "../../lib/detectOs";
+import { trackDownload } from "../../lib/trackDownload";
import { Section } from "../layout/Section";
import { Button } from "../ui/Button";
import { PlatformLogo } from "../ui/PlatformLogo";
@@ -28,11 +29,24 @@ export function Platforms() {
const hints = useMemo(() => readNavigatorHints(), []);
- const osDownloadHref = useMemo(() => {
+ const { osDownloadHref, detectedOs } = useMemo(() => {
const os = detectOs(hints.userAgent);
- return resolveDownloadHref(os, DOWNLOAD_LINKS, hints);
+ return {
+ osDownloadHref: resolveDownloadHref(os, DOWNLOAD_LINKS, hints),
+ detectedOs: os,
+ };
}, [hints]);
+ const trackOsDownload = () => {
+ const platform =
+ detectedOs === "windows" ||
+ detectedOs === "macos" ||
+ detectedOs === "linux"
+ ? detectedOs
+ : "windows";
+ trackDownload(platform);
+ };
+
const platformDownloadHrefs = useMemo(
() =>
Object.fromEntries(
@@ -58,6 +72,9 @@ export function Platforms() {
href={platformDownloadHrefs[id]}
variant="ghost"
className={styles.downloadBtn}
+ onClick={() => {
+ trackDownload(id);
+ }}
>
{copy.cta.download}
@@ -72,7 +89,9 @@ export function Platforms() {
-
+
);
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index aceada3..2114aa3 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -34,6 +34,7 @@ export function Button({
{
+ afterEach(() => {
+ mock.restoreAll();
+ });
+
+ it("builds default tracking URL", () => {
+ assert.equal(
+ resolveTrackDownloadUrl(DEFAULT_LICENSE_TRACKING_BASE),
+ "https://license.ttrpgplayer.ru/v1/track/download",
+ );
+ });
+
+ it("normalizes base URL without trailing slash", () => {
+ assert.equal(
+ resolveTrackDownloadUrl("https://license.example.test"),
+ "https://license.example.test/v1/track/download",
+ );
+ });
+
+ it("POSTs platform with keepalive and swallows errors", async () => {
+ const fetchMock = mock.fn(() => Promise.reject(new Error("network down")));
+ globalThis.fetch = fetchMock;
+
+ trackDownload("macos");
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ assert.equal(fetchMock.mock.calls.length, 1);
+ const call = fetchMock.mock.calls[0];
+ assert.ok(call);
+ const [url, init] = call.arguments as unknown as [string, RequestInit];
+ assert.equal(url, "https://license.ttrpgplayer.ru/v1/track/download");
+ assert.equal(init.method, "POST");
+ assert.equal(init.keepalive, true);
+ assert.deepEqual(init.headers, { "Content-Type": "application/json" });
+ assert.equal(init.body, JSON.stringify({ platform: "macos" }));
+ });
+});
diff --git a/src/lib/trackDownload.ts b/src/lib/trackDownload.ts
new file mode 100644
index 0000000..6ffcbca
--- /dev/null
+++ b/src/lib/trackDownload.ts
@@ -0,0 +1,35 @@
+import type { PlatformId } from "../data/siteContent";
+
+export const DEFAULT_LICENSE_TRACKING_BASE = "https://license.ttrpgplayer.ru/";
+
+function normalizeBaseUrl(base: string): string {
+ return base.endsWith("/") ? base : `${base}/`;
+}
+
+export function resolveLicenseTrackingBase(): string {
+ const fromEnv =
+ typeof import.meta.env !== "undefined"
+ ? import.meta.env.VITE_LICENSE_TRACKING_URL
+ : undefined;
+ if (typeof fromEnv === "string" && fromEnv.trim()) {
+ return normalizeBaseUrl(fromEnv.trim());
+ }
+ return DEFAULT_LICENSE_TRACKING_BASE;
+}
+
+export function resolveTrackDownloadUrl(base?: string): string {
+ return `${normalizeBaseUrl(base ?? resolveLicenseTrackingBase())}v1/track/download`;
+}
+
+export function trackDownload(platform: PlatformId): void {
+ if (typeof fetch === "undefined") return;
+
+ void fetch(resolveTrackDownloadUrl(), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ platform }),
+ keepalive: true,
+ }).catch(() => {
+ // fire-and-forget: ignore network errors
+ });
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..ae3a532 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,9 @@
///
+
+interface ImportMetaEnv {
+ readonly VITE_LICENSE_TRACKING_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}