Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ Collector settings are environment variables. With Docker Compose, add them to t
| `RATE_LIMIT` | `120` | Pings per minute per client (0 disables). Over-limit requests get `429`, keyed on the socket IP |
| `RETENTION_DAYS` | `180` | Auto-delete pings older than this (GDPR Art. 5(1)(e) storage limitation). `0` disables automatic deletion |
| `STATS_TOKEN` | *(empty = public)* | When set, `/v1/stats`, `/v1/overview`, and `/v1/export` require `?token=<value>` or the `X-Stats-Token` header (header preferred - query strings end up in access logs). Also gates `DELETE /v1/packages/{package}` |
| `INGEST_TOKEN` | *(empty = open)* | When set, `/v1/telemetry/ping` requires `X-Statless-Token: <value>` or `Authorization: Bearer <value>`. The SDK sends it from `STATLESS_TELEMETRY_TOKEN` |
| `INGEST_TOKEN` | *(empty = open)* | When set, `/v1/telemetry/ping` requires `X-Statless-Token: <value>` or `Authorization: Bearer <value>`. The SDK sends it from `STATLESS_TELEMETRY_TOKEN` or `configure({ token })` |
| `TELEMETRY_ENABLED` | `true` | When `false`, the collector accepts and silently drops pings (`204`) |
| `CONTROLLER_NAME` | `statless-telemetry operator` | Legal entity or maintainer name displayed in `/privacy` notice |
| `CONTROLLER_CONTACT` | *(empty)* | Contact email or URL for privacy inquiries in `/privacy` notice |
Expand All @@ -408,7 +408,7 @@ Collector settings are environment variables. With Docker Compose, add them to t
| Var | Default | Purpose |
|---|---|---|
| `STATLESS_TELEMETRY_URL` | *(empty = dormant)* | Ingest endpoint URL (e.g. your own `https://telemetry.example.com/v1/telemetry/ping`) |
| `STATLESS_TELEMETRY_TOKEN` | *(empty)* | Sent as `X-Statless-Token` when the collector sets `INGEST_TOKEN` |
| `STATLESS_TELEMETRY_TOKEN` | *(empty)* | Sent as `X-Statless-Token` when the collector sets `INGEST_TOKEN`. Overridden by `configure({ token })` |
| `DO_NOT_TRACK` | *(empty)* | `1` disables the SDK entirely |
| `STATLESS_OPTOUT` | *(empty)* | `1` disables the SDK entirely |

Expand Down
10 changes: 6 additions & 4 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ npm install @statless/telemetry
```ts
import { configure, track } from "@statless/telemetry";

// Configure your collector endpoint (or set STATLESS_TELEMETRY_URL)
configure({ endpoint: "https://telemetry.example.com/v1/telemetry/ping" });
// Configure your collector endpoint and token (or set
// STATLESS_TELEMETRY_URL / STATLESS_TELEMETRY_TOKEN)
configure({ endpoint: "https://telemetry.example.com/v1/telemetry/ping", token: "shared-secret" });

const started = Date.now();
// ... run the command ...
Expand All @@ -25,8 +26,9 @@ void track({

By default, telemetry remains dormant until an endpoint is explicitly configured with
`configure({ endpoint })`, `track({ endpoint })`, or the `STATLESS_TELEMETRY_URL`
environment variable. Private collectors that set `INGEST_TOKEN` are supported with
`STATLESS_TELEMETRY_TOKEN`.
environment variable. A private collector that sets `INGEST_TOKEN` is supported with
`configure({ token })` or `STATLESS_TELEMETRY_TOKEN`; a configured token wins, and the
environment variable is read at call time so it remains the fallback.

## Privacy & Legal Compliance

Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@statless/telemetry",
"version": "0.1.1",
"version": "0.1.2",
"description": "Zero-dependency, cookie-free usage telemetry for developer CLIs and npm packages. Opt-out aware, pseudonymous rotating hashes, works standalone or self-hosted.",
"license": "MIT",
"author": "Dimitrios Xynos",
Expand Down
16 changes: 12 additions & 4 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* ```ts
* import { configure, track } from "@statless/telemetry";
* configure({ endpoint: "https://telemetry.example.com/v1/telemetry/ping" });
* configure({ endpoint: "https://telemetry.example.com/v1/telemetry/ping", token: "shared-secret" });
* const started = Date.now();
* // ... run the command ...
* void track({ package: "mytool", version: "1.2.3", command: "build", durationMs: Date.now() - started });
Expand Down Expand Up @@ -52,19 +52,27 @@ export interface TrackOptions {
export interface TelemetryConfig {
/** Collector endpoint. Defaults to `STATLESS_TELEMETRY_URL` env var. */
endpoint?: string;
/**
* Sent as `X-Statless-Token` when the collector requires one. Falls back to the
* `STATLESS_TELEMETRY_TOKEN` env var, read at call time.
*/
token?: string;
/** Set `false` to disable telemetry for the whole process. */
enabled?: boolean;
}

/** Public hosted collector endpoint reference. */
export const DEFAULT_HOSTED_ENDPOINT = "https://telemetry.statless.dev/v1/telemetry/ping";
export const DEFAULT_HOSTED_ENDPOINT = "https://in.statless.dev/v1/telemetry/ping";

let endpoint = endpointOverride();
let configuredToken: string | undefined;
let enabled = true;

/** Adjust the endpoint or turn telemetry off for the whole process. */
/** Adjust the endpoint, token, or turn telemetry off for the whole process. */
export function configure(config: TelemetryConfig): void {
if (typeof config.endpoint === "string") endpoint = config.endpoint;
// An empty string clears the override so the env var (read at call time) applies.
if (typeof config.token === "string") configuredToken = config.token || undefined;
if (typeof config.enabled === "boolean") enabled = config.enabled;
}

Expand Down Expand Up @@ -100,5 +108,5 @@ export function track(options: TrackOptions): Promise<void> {
payload.duration_ms = Math.max(0, Math.round(options.durationMs));
}

return send(targetEndpoint, payload, ingestToken());
return send(targetEndpoint, payload, configuredToken ?? ingestToken());
}
23 changes: 21 additions & 2 deletions packages/sdk/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ beforeEach(() => {
delete process.env.STATLESS_TELEMETRY_TOKEN;
delete process.env.CI;
delete process.env.GITHUB_ACTIONS;
configure({ enabled: true, endpoint: TEST_ENDPOINT });
configure({ enabled: true, endpoint: TEST_ENDPOINT, token: "" });
});

afterEach(() => {
Expand Down Expand Up @@ -73,6 +73,25 @@ describe("track", () => {
);
});

it("sends the token set via configure", async () => {
configure({ token: "from-config" });
const { calls } = mockFetch();
await track({ package: "my-cli", version: "1.0.0" });
expect((calls[0]!.init.headers as Record<string, string>)["x-statless-token"]).toBe(
"from-config",
);
});

it("prefers the configured token over the environment", async () => {
process.env.STATLESS_TELEMETRY_TOKEN = "from-env";
configure({ token: "from-config" });
const { calls } = mockFetch();
await track({ package: "my-cli", version: "1.0.0" });
expect((calls[0]!.init.headers as Record<string, string>)["x-statless-token"]).toBe(
"from-config",
);
});

it("omits optional fields when not provided", async () => {
const { calls } = mockFetch();
await track({ package: "my-cli", version: "1.0.0" });
Expand Down Expand Up @@ -153,6 +172,6 @@ describe("track", () => {
expect(isOptedOut()).toBe(false);
process.env.STATLESS_OPTOUT = "1";
expect(isOptedOut()).toBe(true);
expect(DEFAULT_HOSTED_ENDPOINT).toBe("https://telemetry.statless.dev/v1/telemetry/ping");
expect(DEFAULT_HOSTED_ENDPOINT).toBe("https://in.statless.dev/v1/telemetry/ping");
});
});
Loading