diff --git a/COMMANDS.md b/COMMANDS.md index 6697812..302f34e 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -645,7 +645,7 @@ altertable query show|cancel | `--layout ` | Human layout: auto, table, or line Values: auto, table, line. | | `--columns ` | Comma-separated columns to show | | `--max-width ` | Maximum display width for table columns Default: "32". | -| `--compute-size ` | Compute size for the query Values: XS, S, M, L, XL, AUTO. Default: "AUTO". | +| `--compute-size ` | Compute size for the query Default: "AUTO". | | `--dialect ` | Source SQL dialect to transpile from (server default: DuckDB) | | `--catalog ` | Catalog name (optional; can also come from the session) | | `--schema ` | Schema name (optional; can also come from the session) | diff --git a/cli-reference.json b/cli-reference.json index c8be710..9cbaa27 100644 --- a/cli-reference.json +++ b/cli-reference.json @@ -1172,19 +1172,12 @@ { "name": "compute-size", "aliases": [], - "type": "enum", + "type": "string", "description": "Compute size for the query", "required": false, "repeatable": false, "scope": "command", - "values": [ - "XS", - "S", - "M", - "L", - "XL", - "AUTO" - ], + "values": [], "default": "AUTO" }, { diff --git a/cli/src/lib/lakehouse/query.ts b/cli/src/lib/lakehouse/query.ts index a8036ba..44fa931 100644 --- a/cli/src/lib/lakehouse/query.ts +++ b/cli/src/lib/lakehouse/query.ts @@ -9,14 +9,11 @@ import { STREAM_READ_TIMEOUT_MS } from "@/lib/transport-defaults.ts"; export type LakehouseApiQueryFormat = "csv" | "jsonl" | "parquet"; -export const LAKEHOUSE_COMPUTE_SIZES = ["XS", "S", "M", "L", "XL", "AUTO"] as const; -export type LakehouseComputeSize = (typeof LAKEHOUSE_COMPUTE_SIZES)[number]; - export type LakehouseQueryInput = { statement: string; queryId?: string; sessionId?: string; - computeSize?: LakehouseComputeSize; + computeSize?: string; format?: LakehouseApiQueryFormat; dialect?: string; catalog?: string; diff --git a/cli/src/lib/query-output-args.ts b/cli/src/lib/query-output-args.ts index b6dacf4..db5e07e 100644 --- a/cli/src/lib/query-output-args.ts +++ b/cli/src/lib/query-output-args.ts @@ -1,7 +1,6 @@ import { asCliArgString } from "@/lib/cli-args.ts"; import { defineArguments } from "@/lib/command.ts"; import { CliError } from "@/lib/errors.ts"; -import { LAKEHOUSE_COMPUTE_SIZES, type LakehouseComputeSize } from "@/lib/lakehouse/query.ts"; import { isApiNativeQueryFormat, parseQueryResultFormat, @@ -68,10 +67,9 @@ export const queryPagerArgs = defineArguments({ export const queryRequestArgs = defineArguments({ "compute-size": { - type: "enum", + type: "string", description: "Compute size for the query", default: "AUTO", - options: [...LAKEHOUSE_COMPUTE_SIZES], }, dialect: { type: "string", @@ -96,7 +94,7 @@ export type QueryOutputOptions = { displayOptions: QueryDisplayOptions; pagerOptions: PagerOptions; outputPath?: string; - computeSize?: LakehouseComputeSize; + computeSize?: string; dialect?: string; catalog?: string; schema?: string; @@ -192,22 +190,11 @@ function optionalTrimmedString(args: Record, name: string): str return trimmed === "" ? undefined : trimmed; } -function isLakehouseComputeSize(value: string): value is LakehouseComputeSize { - return (LAKEHOUSE_COMPUTE_SIZES as readonly string[]).includes(value); -} - -function parseLakehouseComputeSize(value: string): LakehouseComputeSize { - if (!isLakehouseComputeSize(value)) { - throw new CliError(`--compute-size must be one of ${LAKEHOUSE_COMPUTE_SIZES.join(", ")}.`); - } - return value; -} - export function resolveQueryComputeSize(options: { sessionId?: string; - computeSizeArg?: LakehouseComputeSize; + computeSizeArg?: string; computeSizeExplicit: boolean; -}): LakehouseComputeSize | undefined { +}): string | undefined { const computeSize = options.computeSizeArg ?? "AUTO"; if (options.sessionId && !options.computeSizeExplicit) return undefined; @@ -228,9 +215,7 @@ export function parseQueryOutputOptions( const computeSize = resolveQueryComputeSize({ sessionId, computeSizeArg: - args["compute-size"] === undefined - ? undefined - : parseLakehouseComputeSize(asCliArgString(args["compute-size"])), + args["compute-size"] === undefined ? undefined : asCliArgString(args["compute-size"]), computeSizeExplicit: hasArgvFlag(options.rawArgs, "--compute-size"), }); diff --git a/specs b/specs new file mode 160000 index 0000000..3790834 --- /dev/null +++ b/specs @@ -0,0 +1 @@ +Subproject commit 3790834492fafa446a11145eeda765979055968b diff --git a/specs/.gitignore b/specs/.gitignore deleted file mode 100644 index a5f6d8b..0000000 --- a/specs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -.cursor/ diff --git a/specs/AGENTS.md b/specs/AGENTS.md deleted file mode 100644 index cf53edf..0000000 --- a/specs/AGENTS.md +++ /dev/null @@ -1,81 +0,0 @@ -# AGENTS.md - Client Specs - -This is the source of truth for Altertable SDK specifications. It is version-controlled and public. Every change is reviewed by the team via pull request. - -## What this repo is - -A pure specs repository: requirements, fixtures, constants, and test plans. No runtime code lives here. SDK repositories consume this as a pinned git submodule. The workspace bot (Albert) reads these specs to implement and update SDKs. - -## Repository layout - -``` -├── AGENTS.md # This file -├── README.md # Public overview and submodule usage guide -├── http/ -│ └── SPEC.md # HTTP transport requirements (shared by all SDKs) -├── lakehouse/ -│ └── SPEC.md # Lakehouse API client spec -├── product-analytics/ -│ ├── SPEC.md # Product Analytics SDK spec -│ ├── CONSTANTS.md # Shared constants (storage keys, timing, event names) -│ ├── TEST_PLAN.md # Test plan for all SDK tiers -│ └── fixtures/ # JSON fixtures for unit and integration tests -└── rest/ - └── SPEC.md # Management REST API spec (opt-in: CLI + Terraform only) -``` - -## Contribution rules - -### Treat spec changes as API changes - -Every change here affects downstream SDKs that are already in production. Apply the same discipline you would to a public API: - -- **Patch** (`v0.1.x`): Fix typos, clarify wording, add examples — no behavioral change. -- **Minor** (`v0.x.0`): Add new optional fields, new phases, new fixtures — backwards-compatible. -- **Major** (`vx.0.0`): Remove or rename fields, change required behavior, break existing SDK implementations. - -### Always update tests and fixtures together with the spec - -Never update `SPEC.md` without also updating: -- `TEST_PLAN.md` — reflect new or changed behaviors -- `fixtures/` — add or update JSON fixtures that validate the change -- `CONSTANTS.md` — update constants if values or names changed - -### Tag every release - -After merging a spec change, tag the commit with a semver version: - -```bash -git tag v0.2.0 -git push origin v0.2.0 -``` - -SDK repositories pin to a tag — never a branch. Once a tag is pushed, treat it as immutable. - -### Coordinate with the workspace after tagging - -Albert detects new tags on each heartbeat poll by running `spec-status.sh` locally. It compares each SDK's pinned submodule against the latest tag in this repo and opens submodule-update PRs for any lagging SDK. - -**Loop closure**: The spec-sync loop is not closed at "PR opened". It is closed only when every downstream repo in the workspace inventory is accounted for: either updated (PR merged), has an open update PR, or has an explicit blocker issue. Albert creates or updates a tracking issue with `spec-update` or `spec-outdated` until all repos are accounted for. - -**Expected outcome**: Within the next heartbeat cycle, Albert will open PRs updating each outdated SDK to the new spec version. - -**If no PRs appear within 24 hours**: -1. Check open issues labeled `spec-update` or `spec-outdated` in [albert-workspace](https://github.com/altertable-ai/albert-workspace) — if found, Albert is aware but blocked; check the issue for details and escalate to the team. -2. If no such issues exist, in albert-workspace run `bash scripts/spec-status.sh` to verify the drift is detectable. If the script reports outdated SDKs but no tracking issue exists, open an issue in albert-workspace to investigate (Albert may be down or the heartbeat may need attention). - -### Never modify files inside `specs/` of an SDK repo directly - -The `specs/` submodule in each SDK repo is read-only. All changes flow from this repo → tag → submodule update PR. - -## Branch and PR conventions - -- Branch naming: `feat/` or `fix/-` -- Commit messages: follow [Conventional Commits](https://www.conventionalcommits.org/) -- PR titles must also follow Conventional Commits, because Altertable repositories squash-merge and release-please uses the merged PR title as the release signal. Repositories that use release-please must enforce this with a GitHub Actions check using `amannn/action-semantic-pull-request@v5` on `pull_request_target` for `opened`, `edited`, and `synchronize`. -- PR description: state which SDKs are affected and whether it is a breaking change - -## Links - -- Workspace (Albert): [altertable-ai/albert-workspace](https://github.com/altertable-ai/albert-workspace) -- SDK repositories: see [sdk-sync inventory](https://github.com/altertable-ai/albert-workspace/blob/main/skills/sdk-sync/SKILL.md#repository-inventory) diff --git a/specs/LICENSE b/specs/LICENSE deleted file mode 100644 index ffca092..0000000 --- a/specs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025-present Altertable - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/specs/README.md b/specs/README.md deleted file mode 100644 index edbe178..0000000 --- a/specs/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Altertable Client Specs - -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) - -Versioned API specifications and agent skills for building and maintaining Altertable open-source SDKs. - -## Overview - -This repository contains pure specifications: requirements, fixtures, constants, and test plans. SDK repositories consume it as a git submodule. Workspace skills read these specs and act on them. - -SDK repositories pin a specific version tag of this repo via a `specs/` submodule, ensuring every SDK is built against a known, reproducible spec snapshot. - -## Specs - -| Skill | Description | -|---|---| -| [`bootstrap-sdk`](skills/bootstrap-sdk/SKILL.md) | Fork, clone, and wire up a new SDK repo or update an existing one to a new spec version | -| [`build-lakehouse-sdk`](skills/build-lakehouse-sdk/SKILL.md) | Build a production-grade Altertable Lakehouse API client in any language | -| [`build-product-analytics-sdk`](skills/build-product-analytics-sdk/SKILL.md) | Build an Altertable Product Analytics SDK with identity, event tracking, and auto-capture | -| [`build-http-sdk`](skills/build-http-sdk/SKILL.md) | HTTP client best practices — connection pooling, keep-alive, timeouts (referenced by build-* skills) | -| [`build-readme`](skills/build-readme/SKILL.md) | Write READMEs for SDK repos and monorepo roots following Altertable conventions | -| [`maintainer-routine`](skills/maintainer-routine/SKILL.md) | Notification-driven maintainer routine to identify actionable work across Altertable SDK repositories | -| [`release-sdk`](skills/release-sdk/SKILL.md) | Release SDKs, write changelogs, and publish to language registries | -| [`review-pr`](skills/review-pr/SKILL.md) | Review community pull requests against Altertable SDK standards | -| [`sync-repos`](skills/sync-repos/SKILL.md) | Keep shared configuration, community files, and CI templates consistent across SDK repositories | -| [`triage-issues`](skills/triage-issues/SKILL.md) | Triage incoming GitHub issues across Altertable SDK repositories | - -## Using This Repo as a Submodule - -To consume a pinned version of these specs in an SDK repository: - -```bash -git submodule add https://github.com/altertable-ai/altertable-client-specs.git specs -git -C specs checkout -git add .gitmodules specs -git commit -m "chore: add altertable-client-specs submodule at " -``` - -After cloning an SDK repo that already includes this submodule: - -```bash -git submodule update --init --recursive -``` - -## Versioning - -Spec versions follow [Semantic Versioning](https://semver.org). Each tag (e.g. `v0.1.0`) represents a stable, immutable snapshot. SDK repositories pin to a tag — never a branch — to guarantee reproducible builds. - -## Workspace - -Albert, the autonomous AI maintainer of these SDKs, operates from the [albert-workspace](https://github.com/altertable-ai/albert-workspace) repository. Operational skills (triage, review, release, sync) live there. - -## Contributing - -1. Fork this repository -2. Create a branch: `feat/` or `fix/-` -3. Commit your changes with a clear message -4. Push to your fork -5. Open a pull request against `main` - -## License - -[MIT](LICENSE) - -## Links - -- Website: [https://altertable.ai](https://altertable.ai) -- Documentation: [https://altertable.ai/docs](https://altertable.ai/docs) -- GitHub: [https://github.com/altertable-ai/altertable-client-specs](https://github.com/altertable-ai/altertable-client-specs) diff --git a/specs/http/SPEC.md b/specs/http/SPEC.md deleted file mode 100644 index 24a5d9e..0000000 --- a/specs/http/SPEC.md +++ /dev/null @@ -1,189 +0,0 @@ -# HTTP Transport Specification - -This specification defines HTTP client performance best practices and language-specific recommendations for building production-grade Altertable SDKs. Consult this spec when implementing HTTP transport layers for Lakehouse or Product Analytics clients. - -## Core Principles - -### 1. Connection Pooling and Keep-Alive - -**Enable HTTP connection keep-alive by default:** - -- Reuse TCP connections across multiple HTTP requests -- Reduce latency by avoiding repeated TCP handshakes - -### 2. Default Timeouts - -**Always set sensible default timeouts:** - -- **Connect timeout**: 5 seconds (time to establish TCP connection) -- **Read/response timeout**: 60 seconds (time to receive complete response) - -**Document timeout behavior clearly:** - -- Explain what each timeout controls -- Provide examples of how to override defaults -- Warn about long-polling or streaming endpoints that may need longer timeouts - -**Allow per-request timeout overrides:** - -- Some operations may need different timeout characteristics -- Streaming queries may need no read timeout or very long timeouts -- Batch operations may need extended timeouts - -## Language-Specific HTTP Client Recommendations - -Choose HTTP clients that provide excellent keep-alive support and production-ready reliability. - -### Ruby - -**Preference order:** - -1. **`httpx`** (recommended) - - - HTTP/2 support - - Excellent connection pooling - -2. **`faraday`** (fallback) - - - Adapter pattern for flexibility - - Rich middleware ecosystem - - Wide adoption and stability - - Good connection pooling via adapters - -3. **`Net::HTTP`** (last resort) - - Standard library (no dependencies) - - Limited connection pooling capabilities - - Requires more manual configuration - -### Python - -**Preference order:** - -1. **`httpx`** (recommended) - - - Async support (sync API also available) - - HTTP/2 support - - Connection pooling built-in - - Timeout configuration per request - -2. **`requests`** (fallback) - - Ubiquitous and simple API - - `requests.Session` for connection pooling - - Wide ecosystem support - -### JavaScript/TypeScript - -**Preference order:** - -1. **`fetch` API** (recommended for Node 18+) - - - Native support with automatic keep-alive - - Modern promise-based API - - No dependencies - -2. **`undici`** or **`node-fetch`** (for Node <18) - - - `undici`: High-performance, official Node.js fetch implementation - - `node-fetch`: Polyfill for fetch API - -3. **`axios`** (for interceptor patterns) - - Rich interceptor ecosystem - - Automatic retries via plugins - - Wide adoption - -### Go - -**Recommendation:** - -Use `net/http` standard library with a properly configured `http.Client` and custom `Transport`. - -### Java - -**Preference order:** - -1. **`java.net.http.HttpClient`** (recommended for Java 11+) - - - Native HTTP/2 support - - Connection pooling built-in - - Modern async API - -2. **`OkHttp`** (for Java 8-10 or advanced features) - - Robust connection pooling - - Interceptor support - - Automatic retries - - Wide adoption - -### Rust - -**Recommendation:** - -Use `reqwest` with connection pooling enabled. - -### Swift - -**Preference order:** - -1. **`URLSession`** with custom configuration (recommended) - - - Native platform support - - HTTP/2 support - - Built-in connection pooling - -2. **`Alamofire`** (for advanced features) - - Rich interceptor ecosystem - - Request/response serialization - - Automatic retries - -### Kotlin - -**Preference order:** - -1. **`Ktor Client`** (recommended) - - - Multiplatform support (JVM, Android, iOS, JS) - - Async/coroutine support - - Connection pooling built-in - - Plugin-based architecture - -2. **`OkHttp`** (fallback for Android/JVM) - - Industry standard on Android - - Robust connection pooling - - Interceptor support - - Automatic retries - -### PHP - -**Recommendation:** - -Use `Guzzle` with connection pooling configuration. - -## Integration with SDK - -**When integrating HTTP client into SDK:** - -1. **Make the HTTP client configurable:** - - - Allow users to choose from the supported HTTP client - - Provide sensible defaults - - Document how to customize the HTTP client - -2. **Expose configuration options:** - - - Timeouts (connect, read, total) - - Connection pool settings - - Proxy configuration - -3. **Document performance characteristics:** - - Expected request latencies - - Connection pool sizing guidance - - When to use custom timeouts - - Streaming vs. buffered responses - -## Acceptance Checklist - -Only consider the HTTP transport layer complete when: - -- [ ] Connection keep-alive is enabled by default -- [ ] Sensible timeout defaults are configured -- [ ] Timeout and retry behavior is documented -- [ ] Language-appropriate HTTP client is used diff --git a/specs/lakehouse/SPEC.md b/specs/lakehouse/SPEC.md deleted file mode 100644 index 053180a..0000000 --- a/specs/lakehouse/SPEC.md +++ /dev/null @@ -1,351 +0,0 @@ -# Lakehouse API Client Specification - -This specification defines requirements for implementing a language-idiomatic, strongly typed (or as strongly typed as idiomatic for the target language—best effort for dynamic or scripting languages), open-source client for the Altertable Lakehouse API. - -Primary OpenAPI specification reference: `https://api.altertable.ai/openapi/lakehouse.json` - -## Required Outcomes - -1. Full endpoint coverage: `append` (including optional synchronous completion - and task polling), `GET /tasks/{task_id}`, `query` (streamed and - accumulated), `GET`/`DELETE /query/{query_id}`, `upload`, `upsert`, - `validate`, and `autocomplete`. -2. Package is publishable to the target language's primary registry. -3. Typed models and typed errors are first-class. -4. `/query` exposes both streamed (with metadata, columns, and row iterator) and accumulated (with all rows) versions. -5. Tests provide confidence in real runtime behavior. -6. Project is modern OSS with MIT licensing. - -## Requirements - -Follow these phases in order. - -### Phase 1: Scaffold - -1. Create package/module scaffold with idiomatic structure. -2. Add MIT license. -3. Add README, changelog, and contribution docs. -4. Configure lint/typecheck/test scripts. -5. Generate a comprehensive `.gitignore` using `https://www.toptal.com/developers/gitignore/api/{language}` as a reference. - -### Phase 2: Models and Serialization - -1. Generate or define request/response models from OpenAPI. -2. Preserve enums and nullable semantics: - - `ComputeSize`: `XS | S | M | L | XL` - - `UploadMode`: `create | append | overwrite` - - `AppendErrorCode`: `invalid-data | incompatible-schema` - - `TaskStatus`: `pending | completed` - - `SessionKind` (for `QueryLog.client_interface`): `ArrowFlightSQL | HttpQuery | HttpCancel | HttpValidate | HttpExplain | HttpAutocomplete | Postgres` -3. Preserve `oneOf` behavior for `AppendRequest` exactly as in OpenAPI: the JSON body is **either** a single `AppendPayload` object **or** a JSON array of `AppendPayload` objects. -4. Model `AppendResponse` per OpenAPI: required `ok`; nullable `error_code` (`AppendErrorCode` or null); nullable `error_message`; nullable `task_id` (UUID) for polling via `GET /tasks/{task_id}`. - -### Phase 3: Client Core - -Implement a configurable client constructor/factory with: - -- `baseUrl` (default `https://api.altertable.ai`) -- auth options (see Authentication) -- timeout configuration -- retry policy -- optional user-agent suffix - -### Phase 4: Endpoint Methods - -Implement typed methods for all operations: - -1. `append` - - - `POST /append` - - required query params: `catalog`, `schema`, `table` - - optional query param: `sync` — when true, the server waits for the append task to finish before returning - - JSON request body: `AppendRequest` - - typed response: `AppendResponse` (`ok`, nullable `error_code`, nullable `error_message`, nullable `task_id`) - -2. `getTask` (or `get_task`) - - - `GET /tasks/{task_id}` - - path param: `task_id` (UUID), as returned by `append` when processing is asynchronous - - typed response: `TaskResponse` (`task_id`, `status` where `status` is `TaskStatus`) - -3. `query` - - Two versions must be provided: - - a. `query` (streamed) - - - `POST /query` - - JSON body: `QueryRequest` (must include `statement`) - - content type: `application/x-ndjson` - - returns a structured result containing: - - metadata - - columns - - an enumerator/iterator/async iterator/observable/channel to iterate over streamed rows - - b. `queryAll` (or `query_all`, accumulated) - - - `POST /query` - - JSON body: `QueryRequest` (must include `statement`) - - accumulates all rows from the stream before returning - - returns a structured result containing: - - metadata - - columns - - all rows as an array/list/collection - -4. `upload` - - - `POST /upload` - - required query params: `catalog`, `schema`, `table`, `mode` - - `mode` is an `UploadMode` (`create`, `append`, or `overwrite`) - - body: raw file bytes or stream; set `Content-Type` when the format is - known (CSV, JSON, or Parquet). When omitted, the server infers format from - magic bytes. - -5. `upsert` - - - `POST /upsert` - - required query params: `catalog`, `schema`, `table`, `primary_key` - - `primary_key` is the column name used to match existing rows before - updating them - - body: raw file bytes or stream; set `Content-Type` when the format is - known (CSV, JSON, or Parquet). When omitted, the server infers format from - magic bytes. - -6. `getQuery` (or `get_query`) - - - `GET /query/{query_id}` - - path param: `query_id` (UUID) - - typed response: `QueryLogResponse` - - returns query log information including stats, progress, duration, error - -7. `cancelQuery` (or `cancel_query`) - - - `DELETE /query/{query_id}` - - path param: `query_id` (UUID) - - required query param: `session_id` - - typed response: `CancelQueryResponse` - - cancels a running query - -8. `validate` - - - `POST /validate` - - JSON body: `ValidateRequest` (must include `statement`) - - typed response: `ValidateResponse` - -9. `autocomplete` - - - `POST /autocomplete` - - JSON body: `AutocompleteRequest` (must include `statement`) - - typed response: `AutocompleteResponse` - -### Phase 5: Streaming Contract (`query`) - -The streamed `query` method must parse the NDJSON response and return a structured result with: - -1. **metadata** - Query metadata (parsed from the first JSON object line). Parsers must accept the fields defined in OpenAPI (non-exhaustive examples aligned with the published spec): `statement`, `rows_limit`, `rows_offset`, `init_time_ms`, `connections_errors`, `session_id`, `query_id`, `worker_slug`. Treat unknown keys as forward-compatible passthrough or opaque map entries where idiomatic. -2. **columns** - Column schema information (parsed when schema row appears) -3. **rows iterator** - An enumerator/iterator/async iterator/observable/channel to iterate over streamed rows - -The accumulated `queryAll` method should: - -- Call `query` with the same request and accumulate the rows into a single array/list/collection -- Return metadata, columns, and all rows - -Requirements: - -- Include line index/context in parsing failures. -- Never silently ignore malformed lines. -- Preserve backpressure semantics of the language runtime (for streamed version). - -### Phase 6: Authentication - -The Altertable Lakehouse API uses standard HTTP Basic Auth. Credentials are sent in every request as: - -``` -Authorization: Basic -``` - -The client must support all of the following credential input patterns: - -1. **Direct credentials** — `username` + `password` accepted in the client constructor/config; the SDK encodes them into the Basic token internally. -2. **Pre-encoded token** — accept a raw pre-encoded `Basic` token string directly for callers who already hold the encoded value. -3. **Environment variable discovery** — auto-discover from the environment: - - `ALTERTABLE_LAKEHOUSE_USERNAME` + `ALTERTABLE_LAKEHOUSE_PASSWORD` - (encode on the fly), or - - `ALTERTABLE_BASIC_AUTH_TOKEN` (use directly as the pre-encoded value) - -Implementation requirements: - -- Credentials must never appear in logs, error messages, or debug output. -- A `ConfigurationError` must be raised at construction time when no credentials can be resolved. - -### Phase 7: Error Model - -Implement a typed error hierarchy at minimum: - -- `AuthError` -- `BadRequestError` -- `NetworkError` -- `TimeoutError` -- `SerializationError` -- `ParseError` -- `ApiError` (unexpected status fallback) -- `ConfigurationError` - -All errors should include, when available: - -- operation name -- HTTP method/path -- status code -- retriable flag/classification -- request/correlation id headers -- underlying cause - -### Phase 8: Transport and Reliability - -**For HTTP client performance best practices**, including keep-alive, timeout defaults, and language-specific HTTP client recommendations, read and follow the [HTTP transport spec](../http/SPEC.md). - -### Phase 9: Testing - -Implement layered tests: - -1. Unit tests - - - model serialization - - request construction - - auth behavior and redaction - - retries/timeouts - - input precondition checks - -2. Integration tests — run against `ghcr.io/altertable-ai/altertable-mock:latest` - - The mock server speaks the full Altertable Lakehouse API. Credentials are configured via the `ALTERTABLE_MOCK_USERS` environment variable, server listens on port `15000`. - - Use a **dual-mode** approach so tests always run — both locally and in CI — without real credentials: - - **In CI (GitHub Actions):** declare the mock as a service container so it is pre-bound to `localhost:15000` before the test step starts: - - ```yaml - services: - altertable: - image: ghcr.io/altertable-ai/altertable-mock:latest - ports: - - 15000:15000 - env: - ALTERTABLE_MOCK_USERS: testuser:testpass - options: >- - --health-cmd "exit 0" - --health-interval 5s - --health-timeout 3s - --health-retries 3 - --health-start-period 10s - ``` - - **Outside CI (local development):** use the language-native Testcontainers library to pull and start the mock automatically before the test suite, store the mapped port in an environment variable (e.g. `ALTERTABLE_MOCK_PORT`), and stop the container via an `at_exit` / teardown hook. Skip this step when the `CI` environment variable is set. - - The test base URL is always `http://localhost:${ALTERTABLE_MOCK_PORT:-15000}`. Point every test client instance at this URL. - - Cover at minimum: - - - one streamed `query` call verifying metadata (including documented metadata keys where the mock emits them), columns, and row iteration - - one `queryAll` call verifying all rows are accumulated - - one `getQuery` call verifying the query log response - - one `cancelQuery` call verifying the cancellation response - - one `upload` call using `mode=create`, `append`, or `overwrite` (CSV, JSON - or Parquet payload with an appropriate `Content-Type`, or rely on - server-side format inference) - - one `upsert` call with `primary_key` (CSV, JSON or Parquet payload with an - appropriate `Content-Type`, or rely on server-side format inference) - - one `validate` call - - one `append` call - - one `getTask` call when the mock exposes a task id (or append returns `task_id`), verifying `TaskResponse` - - one `autocomplete` call verifying suggestions and `connections_errors` - -CI should always run lint + typecheck + unit + integration tests (mock-backed). No test should be skipped due to missing credentials. - -### Packaging requirements - -1. Include examples for all operations (`append`, `getTask`, `query`, - `queryAll`, `getQuery`, `cancelQuery`, `upload`, `upsert`, `validate`, - `autocomplete`) in the README. -2. Verify docs match runtime behavior. - -## Endpoint Reference (Minimal) - -### `POST /append` - -- Query: `catalog`, `schema`, `table`, optional `sync` -- Body: `AppendRequest` -- Response: `AppendResponse` — required `ok`; nullable `error_code` (`invalid-data` \| `incompatible-schema` \| null); nullable `error_message`; nullable `task_id` (UUID) for `GET /tasks/{task_id}` - -### `GET /tasks/{task_id}` - -- Path: `task_id` (UUID) -- Response: `TaskResponse` with `task_id` and `status` (`pending` \| `completed`) -- Status codes: 200, 400 (invalid task id), 401 - -### `POST /query` - -- Body: `QueryRequest` -- Response: NDJSON stream -- Key request fields: - - required: `statement` - - optional: `catalog`, `schema`, `session_id`, `compute_size`, `sanitize`, `limit`, `offset`, `timezone`, `ephemeral`, `visible`, `requested_by`, `query_id`, `cache` - -### `POST /upload` - -- Query: `catalog`, `schema`, `table`, `mode` -- Mode: `create` | `append` | `overwrite` -- Body: binary file content -- Format: not a query parameter. The server infers CSV, JSON, or Parquet from - the `Content-Type` header when present, otherwise from magic bytes in the - payload. - -### `POST /upsert` - -- Query: `catalog`, `schema`, `table`, `primary_key` -- Body: binary file content -- Format: not a query parameter. The server infers CSV, JSON, or Parquet from - the `Content-Type` header when present, otherwise from magic bytes in the - payload. - -### `GET /query/{query_id}` - -- Path: `query_id` (UUID) -- Response: `QueryLogResponse` containing query log information -- Returns: query metadata including `uuid`, `start_time`, `end_time`, `duration_ms`, `query`, `session_id`, `client_interface` (`SessionKind`), `error`, `stats` (with `caching`, `memory`, `scan`), `progress`, `visible`, `requested_by`, `user_agent` -- Status codes: 200 (success), 401 (auth required), 404 (query not found) - -### `DELETE /query/{query_id}` - -- Path: `query_id` (UUID) -- Query: `session_id` (required) -- Response: `CancelQueryResponse` with `cancelled` (boolean) and `message` (string) -- Status codes: 200 (success), 400 (invalid request), 401 (auth required), 404 (session not found) -- Cancels a running query associated with the given session - -### `POST /validate` - -- Body: `ValidateRequest` with required `statement` -- Response: `ValidateResponse` with `valid`, `statement`, `connections_errors`, optional `error` - -### `POST /autocomplete` - -- Body: `AutocompleteRequest` with required `statement`; optional `catalog`, `schema`, `session_id`, `max_suggestions` -- Response: `AutocompleteResponse` with `suggestions`, `statement`, `connections_errors` -- Status codes: 200, 400, 401 - -## Acceptance Checklist - -Only mark implementation complete when all are true: - -- [ ] All operations in Phase 4 implemented and documented (`append`, - `getTask`, `query` streamed and accumulated, `getQuery`, `cancelQuery`, - `upload`, `upsert`, `validate`, `autocomplete`) -- [ ] Streamed `query` returns metadata, columns, and row iterator; accumulated `queryAll` returns metadata, columns, and all rows -- [ ] Typed errors are comprehensive and actionable -- [ ] Auth supports direct/env/provider patterns -- [ ] Retries/timeouts/transport hooks are configurable -- [ ] Tests provide real-world confidence via the mock server (runs in both CI and local dev) -- [ ] Package is publish-ready for primary registry -- [ ] MIT license and OSS docs are present diff --git a/specs/product-analytics/CONSTANTS.md b/specs/product-analytics/CONSTANTS.md deleted file mode 100644 index 1792b7d..0000000 --- a/specs/product-analytics/CONSTANTS.md +++ /dev/null @@ -1,162 +0,0 @@ -# SDK Constants Reference - -Canonical constant values for the Altertable Product Analytics SDK. Use these exact values when implementing any tier. - -## Storage Keys - -| Constant | Value | -| ---------------------- | -------- | -| `STORAGE_KEY_PREFIX` | `"atbl"` | -| `STORAGE_KEY_SEPARATOR`| `"."` | - -Storage key format: `atbl.{apiKey}.{environment}` - -`STORAGE_KEY_TEST` is built as `atbl.check` — used to verify storage availability. - -## ID Prefixes - -| Constant | Value | -| --------------------- | ------------- | -| `PREFIX_SESSION_ID` | `"session"` | -| `PREFIX_ANONYMOUS_ID` | `"anonymous"` | -| `PREFIX_DEVICE_ID` | `"device"` | - -## Timing and Limits - -| Constant | Value | Notes | -| ---------------------------- | ----------- | -------------------------- | -| `AUTO_CAPTURE_INTERVAL_MS` | `100` | SPA URL polling interval | -| `SESSION_EXPIRATION_TIME_MS` | `1_800_000` | 30 minutes in milliseconds | -| `MAX_QUEUE_SIZE` | `1_000` | Drop oldest on overflow | -| `REQUEST_TIMEOUT_MS` | `5_000` | Web tier HTTP timeout | - -## Event and Property Names - -| Constant | Value | -| ---------------------- | ---------------- | -| `EVENT_PAGEVIEW` | `"$pageview"` | -| `PROPERTY_LIB` | `"$lib"` | -| `PROPERTY_LIB_VERSION` | `"$lib_version"` | -| `PROPERTY_REFERER` | `"$referer"` | -| `PROPERTY_RELEASE` | `"$release"` | -| `PROPERTY_URL` | `"$url"` | -| `PROPERTY_VIEWPORT` | `"$viewport"` | - -## Timing and Limits (Mobile) - -| Constant | Value | Notes | -| ----------------------------- | ------- | -------------------------- | -| `MOBILE_REQUEST_TIMEOUT_MS` | `10_000` | Mobile tier HTTP timeout | - -## Config Interfaces and Defaults - -### WebConfig - -| Option | Type | Default | Description | -| ------------------ | ------------------------- | ------------------------- | --------------------------------------------------------------------------------- | -| `apiKey` | `string` | _(required)_ | Public API key (`pk_live_…` or `pk_test_…`) | -| `baseUrl` | `string` | `https://api.altertable.ai` | Override the API base URL | -| `environment` | `string` | `"production"` | Analytics environment name | -| `persistence` | `"localStorage+cookie" \| "localStorage" \| "sessionStorage" \| "cookie" \| "memory"` | `"localStorage+cookie"` | Storage backend | -| `trackingConsent` | `TrackingConsentState` | `"granted"` | Initial tracking consent state | -| `autoCapture` | `boolean` | `true` | Automatically capture pageviews on SPA navigation | -| `release` | `string \| null` | `null` | App release/version string attached to every event as `$release` | -| `onError` | `(error: AltertableError) => void \| null` | `null` | Callback invoked on SDK errors (must not throw) | -| `debug` | `boolean` | `false` | Enable verbose console logging | -| `requestTimeout` | `number` | `REQUEST_TIMEOUT_MS` | HTTP request timeout in milliseconds | - -**`WEB_DEFAULTS`** (canonical values): - -``` -apiKey: (none — required) -baseUrl: "https://api.altertable.ai" -environment: "production" -persistence: "localStorage+cookie" -trackingConsent: "granted" -autoCapture: true -release: null -onError: null -debug: false -requestTimeout: 5000 -``` - -### MobileConfig - -| Option | Type | Default | Description | -| ----------------- | ---------------------- | --------------------------- | ------------------------------------------------------------------ | -| `apiKey` | `string` | _(required)_ | Public API key | -| `baseUrl` | `string` | `https://api.altertable.ai` | Override the API base URL | -| `environment` | `string` | `"production"` | Analytics environment name | -| `trackingConsent` | `TrackingConsentState` | `"granted"` | Initial tracking consent state | -| `release` | `string \| null` | `null` | App release/version string attached to every event as `$release` | -| `onError` | `(error: AltertableError) => void \| null` | `null` | Callback invoked on SDK errors | -| `debug` | `boolean` | `false` | Enable verbose logging | -| `requestTimeout` | `number` | `MOBILE_REQUEST_TIMEOUT_MS` | HTTP request timeout in milliseconds | -| `flushOnBackground` | `boolean` | `true` | Flush queued events when app moves to background | - -**`MOBILE_DEFAULTS`** (canonical values): - -``` -apiKey: (none — required) -baseUrl: "https://api.altertable.ai" -environment: "production" -trackingConsent: "granted" -release: null -onError: null -debug: false -requestTimeout: 10000 -flushOnBackground: true -``` - -### ServerConfig - -| Option | Type | Default | Description | -| ---------------- | ----------------------- | --------------------------- | ----------------------------------------------------------------- | -| `apiKey` | `string` | _(required)_ | Public API key (`pk_live_…` or `pk_test_…`) | -| `baseUrl` | `string` | `https://api.altertable.ai` | Override the API base URL | -| `environment` | `string` | `"production"` | Analytics environment name | -| `release` | `string \| null` | `null` | App release/version string attached to every event as `$release` | -| `onError` | `(error: AltertableError) => void \| null` | `null` | Callback invoked on SDK errors | -| `debug` | `boolean` | `false` | Enable verbose logging | -| `requestTimeout` | `number` | `5000` | HTTP request timeout in milliseconds | -| `maxBatchSize` | `number` | `100` | Maximum number of events per batch request | - -**`SERVER_DEFAULTS`** (canonical values): - -``` -apiKey: (none — required) -baseUrl: "https://api.altertable.ai" -environment: "production" -release: null -onError: null -debug: false -requestTimeout: 5000 -maxBatchSize: 100 -``` - -## Tracking Consent States - -| Constant | Value | Behavior | -| --------------------------- | ------------ | -------------------------------- | -| `TrackingConsent.GRANTED` | `"granted"` | Send events immediately | -| `TrackingConsent.DENIED` | `"denied"` | Drop events, clear queue | -| `TrackingConsent.PENDING` | `"pending"` | Queue events | -| `TrackingConsent.DISMISSED` | `"dismissed"`| Queue events (same as `pending`) | - -## Reserved User IDs - -Reject the following IDs. `RESERVED_USER_IDS` is matched case-insensitively; `RESERVED_USER_IDS_CASE_SENSITIVE` is matched exactly. - -**`RESERVED_USER_IDS`** (case-insensitive): - -``` -anonymous_id, anonymous, distinct_id, distinctid, false, guest, -id, not_authenticated, true, undefined, user_id, user, -visitor_id, visitor -``` - -**`RESERVED_USER_IDS_CASE_SENSITIVE`** (exact match): - -``` -[object Object], 0, NaN, none, None, null -``` diff --git a/specs/product-analytics/SPEC.md b/specs/product-analytics/SPEC.md deleted file mode 100644 index b427a0a..0000000 --- a/specs/product-analytics/SPEC.md +++ /dev/null @@ -1,498 +0,0 @@ -# Product Analytics SDK Specification - -This specification defines requirements for implementing a language-idiomatic, open-source client for the Altertable Product Analytics API. - -OpenAPI specification: https://api.altertable.ai/openapi/product-analytics.json - -Read the OpenAPI spec to manually define typed models and understand request/response schemas. Do not use OpenAPI codegen tools — define models by hand from the spec to keep them idiomatic and minimal. - -## Reference Implementation - -The canonical implementation is the JavaScript/TypeScript SDK in the [`altertable-js` monorepo](https://github.com/altertable-ai/altertable-js). - -Web framework SDKs (React, Vue, Svelte, etc.) belong in this monorepo under `packages/`, not in separate repositories. - -Key files: - -| File | Role | -| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `CONSTANTS.md` (this spec folder) | **SDK constants reference** — all config interfaces, defaults, and internal constants. Single source of truth for values referenced throughout this spec. | -| `packages/altertable-js/src/core.ts` | Main `Altertable` class — init, track, identify, alias, sessions, consent | -| `packages/altertable-js/src/types.ts` | Payload types (`TrackPayload`, `IdentifyPayload`, `AliasPayload`) | -| `packages/altertable-js/src/lib/requester.ts` | Transport — beacon/fetch, URL construction, timeout | -| `packages/altertable-js/src/lib/sessionManager.ts` | Identity state, sessions, consent, device/anonymous/session IDs | -| `packages/altertable-js/src/lib/storage.ts` | Storage abstraction (memory, cookie, localStorage, sessionStorage) | -| `packages/altertable-js/src/constants.ts` | SDK constants (reserved IDs, default values, limits) | -| `packages/altertable-js/src/lib/validateUserId.ts` | Reserved user ID validation | -| `packages/altertable-js/src/lib/error.ts` | Error types and type guards | -| `packages/altertable-js/src/lib/queue.ts` | Pre-init and consent queue | -| `packages/altertable-js/src/lib/safelyRunOnBrowser.ts` | SSR-safe browser API access | -| `packages/altertable-js/test/` | Test patterns (Vitest, custom matchers) | -| `test-utils/` | Shared test helpers (`toRequestApi`, `toWarnDev`, storage mocks) | - -## Platform Tiers - -Feature expectations differ by where the SDK runs. Categorize the target language into a tier: - -| Tier | Languages | Key Expectations | -| ---------- | --------------------------------- | --------------------------------------------------------------------------------------------------------- | -| **Web** | JS/TS | Sessions, auto-capture, storage (localStorage/cookie), beacon transport, tracking consent, pre-init queue | -| **Mobile** | Swift, Kotlin | Sessions, device persistence (Keychain/SharedPrefs), background flush, app lifecycle hooks | -| **Server** | Python, Ruby, Java, Go, PHP, Rust | Stateless per-request tracking, explicit IDs required, batch support, no sessions/storage/auto-capture | - -Use the tier to decide which phases below are required (marked per phase). - -## Required Outcomes - -1. Full endpoint coverage (`track`, `identify`, `alias`). -2. Package is publishable to the target language's primary registry. -3. Typed models and typed errors are first-class (best-effort for dynamic languages). -4. Identity model handles anonymous → identified transitions correctly. -5. Transport is extensible (timeouts, proxy, error hooks). -6. Tests provide confidence in real runtime behavior. -7. MIT license. - -## Requirements - -### Phase 1: Scaffold - -**All tiers.** - -1. Create package scaffold with idiomatic structure. -2. Add MIT license, README, changelog. -3. Configure lint/typecheck/test/build scripts. -4. For web tier: set up bundler (e.g. `tsup`); configure `__DEV__`, `__LIB__`, `__LIB_VERSION__` build-time constants. -5. Generate a comprehensive `.gitignore` using `https://www.toptal.com/developers/gitignore/api/{language}` as a reference. - -### Phase 2: Models and Serialization - -**All tiers.** - -Read the OpenAPI spec and manually define typed request/response models from it — do not use codegen tools. Preserve `oneOf` semantics (single payload or array) for batch support. - -All three methods (`track`, `identify`, `alias`) accept an optional `timestamp` parameter. The API accepts either an ISO 8601 datetime string (e.g. `"2025-06-15T14:30:00.000Z"`) or a Unix epoch integer (seconds). Default to ISO 8601 when generating the timestamp automatically — it's more debuggable. When the caller provides a timestamp explicitly (common for server-side events that sit in a queue before being sent), accept whichever format they supply and pass it through as-is. - -### Phase 3: Client Core - -**All tiers.** - -Implement a configurable client constructor. The full typed config interfaces (`WebConfig`, `MobileConfig`, `ServerConfig`) and their default values (`WEB_DEFAULTS`, `MOBILE_DEFAULTS`, `SERVER_DEFAULTS`) are defined in [CONSTANTS.md](CONSTANTS.md). Use those types and defaults as-is. - -**Server tier**: no sessions, no storage, no auto-capture. The client is stateless. Identity fields (`distinct_id`, `anonymous_id`, `device_id`) are passed explicitly per call — this directly shapes the method signatures for `track`, `identify`, and `alias` on the server tier. See Phase 10 for the exact prototypes. - -**Named/keyword arguments for optional parameters (all tiers):** Never create a method where callers must pass `null` or a positional placeholder to reach a later optional argument. Group all optional parameters into a named options object/struct/dict — or use the language's native keyword argument syntax — so callers can supply any subset in any order without dummy positional values. This applies to both the server-tier `options` objects in Phase 10 and any other method that gains multiple optional parameters in the future. - -### Phase 4: Identity Model - -**All tiers, but implementation differs.** - -The SDK manages three identity concepts: - -| ID | Format | Prefix constant | Purpose | -| -------------- | ----------------------------------------------- | --------------------- | ------------------------------------------------------------------------- | -| `device_id` | `{PREFIX_DEVICE_ID}-{uuid}` | `PREFIX_DEVICE_ID` | Stable device identifier, survives reset | -| `distinct_id` | `{PREFIX_ANONYMOUS_ID}-{uuid}` or user-provided | `PREFIX_ANONYMOUS_ID` | Current identity (anonymous or identified) | -| `anonymous_id` | `{PREFIX_ANONYMOUS_ID}-{uuid}` or `null` | `PREFIX_ANONYMOUS_ID` | Previous anonymous ID after `identify()` — enables backend identity merge | -| `session_id` | `{PREFIX_SESSION_ID}-{uuid}` | `PREFIX_SESSION_ID` | Groups events within an activity window | - -See [CONSTANTS.md](CONSTANTS.md) for prefix values. - -#### State transitions - -1. **Fresh state**: `distinct_id = anonymous-{uuid}`, `anonymous_id = null`, `session_id = session-{uuid}`. -2. **After `identify(user_id)`**: `distinct_id = user_id`, `anonymous_id = previous distinct_id`. -3. **After `reset()`**: New `session_id`, new `anonymous-{uuid}` as `distinct_id`, `anonymous_id = null`. Device ID preserved unless `resetDeviceId: true`. -4. **Re-identify with different user**: Auto-reset first, then identify. - -#### Reserved user IDs - -Reject IDs listed in `RESERVED_USER_IDS` (case-insensitive) and `RESERVED_USER_IDS_CASE_SENSITIVE` (case-sensitive). See [CONSTANTS.md](CONSTANTS.md) for the full lists. - -**Server tier**: No identity state. `distinct_id` and `anonymous_id` are explicit parameters on every call. Validate reserved IDs but don't manage transitions. - -### Phase 5: Session Management - -**Web and mobile tiers only.** - -- Session TTL: `SESSION_EXPIRATION_TIME_MS` (see [CONSTANTS.md](CONSTANTS.md)). -- Renew session (new `session_id`) on first event after TTL expires. -- Persist `lastEventAt` timestamp to detect expiry across page loads / app restarts. -- `session_id` is attached to every `track` payload. - -### Phase 6: Storage and Persistence - -**Web and mobile tiers only.** - -#### Web tier - -Implement a `StorageApi` interface with `getItem`, `setItem`, `removeItem`, `migrate`. - -Backends (with automatic fallback chain): - -1. `localStorage+cookie` (default) — write to both, read from localStorage first -2. `localStorage` -3. `sessionStorage` -4. `cookie` -5. `memory` (final fallback) - -Test storage availability before use. Log warnings on fallback. - -Storage key format: `{STORAGE_KEY_PREFIX}{STORAGE_KEY_SEPARATOR}{apiKey}{STORAGE_KEY_SEPARATOR}{environment}` (see [CONSTANTS.md](CONSTANTS.md) for values). - -Support runtime storage migration when `persistence` config changes via `configure()`. - -#### Mobile tier - -Use platform-native secure storage (Keychain on iOS, EncryptedSharedPreferences on Android). Fallback to standard storage if unavailable. - -**Linux CI Compatibility:** -Mobile SDKs often run unit tests on Linux CI runners (e.g., GitHub Actions `ubuntu-latest`). Platform-specific security frameworks (like `Security.framework` on macOS/iOS) are unavailable on Linux. - -**Requirement:** Abstract your storage layer behind a protocol/interface. - -- **Production:** Inject the concrete secure storage implementation. -- **Linux/CI:** Detect the platform (e.g., `#if os(Linux)`) and inject an **In-Memory** or **No-Op** storage implementation. -- **Do not** simply skip tests. Verify the SDK logic using the in-memory fallback to ensure behavior (identity persistence, queueing) remains correct even without the native secure container. - -### Phase 7: Tracking Consent - -**Web and mobile tiers only.** - -Four states defined by the `TrackingConsentState` type (see [CONSTANTS.md](CONSTANTS.md)): - -| State | Behavior | -| ----------- | -------------------------------------------------- | -| `granted` | Send events immediately | -| `pending` | Queue events, flush when consent becomes `granted` | -| `dismissed` | Queue events (same as `pending`) | -| `denied` | Drop events, clear queue | - -Consent is set at init via `trackingConsent` config and changeable at runtime via `configure({ trackingConsent })`. Persist consent state in storage. - -### Phase 8: Event Queue and Pre-Init Buffering - -**Web and mobile tiers only.** - -Two queuing scenarios: - -1. **Pre-init queue**: `track()`, `identify()`, `alias()`, `page()`, `updateTraits()` called before `init()`. Buffer as commands, replay on init. -2. **Consent queue**: Events generated while consent is `pending`/`dismissed`. Buffer as fully-built payloads, flush when consent becomes `granted`. - -Queue capacity: `MAX_QUEUE_SIZE` (see [CONSTANTS.md](CONSTANTS.md)). Drop oldest on overflow with a warning. - -For pre-init `track`/`page` calls, capture runtime context (timestamp, URL, viewport, referrer) at call time, not at replay time. - -### Phase 9: Auto-Capture - -**Web tier only.** - -When `autoCapture: true`: - -1. Track initial pageview on init. -2. Poll URL every `AUTO_CAPTURE_INTERVAL_MS` to detect SPA navigation (see [CONSTANTS.md](CONSTANTS.md)). -3. Listen for `popstate` and `hashchange` events. -4. On URL change: update referrer to previous URL, fire `EVENT_PAGEVIEW` event. - -`EVENT_PAGEVIEW` properties: `PROPERTY_URL`, `PROPERTY_VIEWPORT`, `PROPERTY_REFERER`, plus extracted URL search params (see [CONSTANTS.md](CONSTANTS.md) for constant values). - -`init()` returns a cleanup function that removes listeners and stops polling. - -`configure({ autoCapture })` toggles auto-capture at runtime. - -### Phase 10: Endpoint Methods - -**All tiers.** - -#### `track` - -**Web/mobile**: `track(event, properties?, options?)` - -`options` (web/mobile, all optional, use named/keyword arguments): - -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -**Server**: `track(event, distinct_id, options?)` - -`options` (server, all optional, use named/keyword arguments): - -- `properties` — event properties dict/object -- `anonymous_id` — pass when forwarding client-side identity context -- `device_id` — pass when forwarding client-side identity context -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -- `POST /track` -- Attach context: `environment`, `device_id`, `distinct_id`, `anonymous_id`, `session_id`, `timestamp`. -- `timestamp` is an optional ISO 8601 string or Unix epoch integer (seconds). If omitted, default to the current time as an ISO 8601 string. -- Merge system properties (`PROPERTY_LIB`, `PROPERTY_LIB_VERSION`, `PROPERTY_RELEASE`, `PROPERTY_URL`) with user properties. User properties win on conflict. See [CONSTANTS.md](CONSTANTS.md) for key values. -- Renew session before sending (web/mobile). -- **Server tier**: `distinct_id` is required (no stored identity). `anonymous_id` and `device_id` are optional — pass them when you have them (e.g. forwarded from a client SDK), omit otherwise. `session_id` is never included (stateless). - -#### `identify` - -**Web/mobile**: `identify(user_id, traits?, options?)` - -`options` (web/mobile, all optional, use named/keyword arguments): - -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -**Server**: `identify(user_id, options?)` - -`options` (server, all optional, use named/keyword arguments): - -- `traits` — user traits dict/object -- `anonymous_id` — pass when forwarding client-side identity context -- `device_id` — pass when forwarding client-side identity context -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -- `POST /identify` -- Transition identity state (web/mobile) or pass IDs explicitly (server). -- `timestamp` is an optional ISO 8601 string or Unix epoch integer (seconds). If omitted, default to the current time as an ISO 8601 string. -- Payload excludes `session_id`. -- **Server tier**: `user_id` becomes `distinct_id` in the payload. `anonymous_id` and `device_id` are optional — supply them when forwarding client-side identity context. - -#### `alias` - -**Web/mobile**: `alias(new_user_id, options?)` - -`options` (web/mobile, all optional, use named/keyword arguments): - -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -**Server**: `alias(distinct_id, new_user_id, options?)` - -`options` (server, all optional, use named/keyword arguments): - -- `timestamp` — ISO 8601 string or Unix epoch integer (seconds); defaults to current time - -- `POST /alias` -- Links `distinct_id` → `new_user_id`. -- `timestamp` is an optional ISO 8601 string or Unix epoch integer (seconds). If omitted, default to the current time as an ISO 8601 string. -- **Server tier**: `distinct_id` is required as the first argument because there is no stored `distinct_id` to link from. It maps directly to `distinct_id` in the payload. - -#### `page(url)` — web tier only - -- Fires `$pageview` track event with parsed URL properties. - -#### `updateTraits(traits)` — web/mobile tiers - -- Sends an identify call with new traits. Requires prior `identify()`. - -#### `reset(options)` — web/mobile tiers - -- Clears session, generates new anonymous identity. -- `resetDeviceId: true` also regenerates device ID. -- Clears the event queue. - -#### `configure(updates)` — web/mobile tiers - -- Updates config at runtime: `autoCapture`, `persistence`, `trackingConsent`. - -#### `getTrackingConsent()` — web/mobile tiers - -- Returns current consent state. - -### Request/Response Examples - -All endpoints accept JSON. Web tier sends the API key as a query param; server tier uses the `X-API-Key` header. - -#### `POST /track?apiKey=pk_live_abc123` - -```json -{ - "timestamp": "2025-06-15T14:30:00.000Z", - "event": "checkout_completed", - "environment": "production", - "device_id": "device-550e8400-e29b-41d4-a716-446655440000", - "distinct_id": "user-42", - "anonymous_id": "anonymous-7c9e6679-7425-40de-944b-e07fc1f90ae7", - "session_id": "session-a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "properties": { - "$lib": "altertable-js", - "$lib_version": "0.3.0", - "$url": "https://example.com/checkout", - "order_total": 99.99 - } -} -``` - -#### `POST /identify?apiKey=pk_live_abc123` - -No `session_id` in the payload. - -```json -{ - "timestamp": "2025-06-15T14:30:00.000Z", - "environment": "production", - "device_id": "device-550e8400-e29b-41d4-a716-446655440000", - "distinct_id": "user-42", - "anonymous_id": "anonymous-7c9e6679-7425-40de-944b-e07fc1f90ae7", - "traits": { - "email": "user@example.com" - } -} -``` - -#### `POST /alias?apiKey=pk_live_abc123` - -```json -{ - "timestamp": "2025-06-15T14:30:00.000Z", - "environment": "production", - "device_id": "device-550e8400-e29b-41d4-a716-446655440000", - "distinct_id": "anonymous-7c9e6679-7425-40de-944b-e07fc1f90ae7", - "anonymous_id": null, - "new_user_id": "user-42" -} -``` - -#### Error response (422) - -```json -{ - "error": "environment-not-found", - "message": "Environment 'staging' not found for this project.", - "details": {} -} -``` - -### Phase 11: Transport - -**All tiers.** - -**For HTTP client performance best practices**, including keep-alive, timeout defaults, and language-specific HTTP client recommendations, read and follow the [HTTP transport spec](../http/SPEC.md). - -#### Web tier - -1. Prefer `navigator.sendBeacon` (fire-and-forget, survives page unload). -2. Fallback to `fetch` with `keepalive: true`. -3. Request timeout: `REQUEST_TIMEOUT_MS` (see [CONSTANTS.md](CONSTANTS.md)). -4. API key sent as query param: `?apiKey={key}`. - -#### Mobile tier - -1. Support background flush on app backgrounding. -2. Request timeout: `MOBILE_REQUEST_TIMEOUT_MS` (see [CONSTANTS.md](CONSTANTS.md)). - -#### Server tier - -1. API key sent via `X-API-Key` header. -2. Support batch payloads natively. - -### Phase 12: Error Model - -**All tiers.** - -Implement typed errors: - -- `AltertableError` — base class -- `ApiError` — HTTP error with `status`, `statusText`, `errorCode`, `details`, request context -- `NetworkError` — connection/timeout failures with `cause` - -Type guards: `isAltertableError()`, `isApiError()`, `isNetworkError()`. - -`onError` config callback receives SDK errors. Never let SDK errors crash the host application. - -Handle `environment-not-found` error specifically: log a warning with a link to the dashboard. - -### Phase 13: Testing - -**All tiers.** - -Implement the mandatory test scenarios defined in [`TEST_PLAN.md`](TEST_PLAN.md). - -Furthermore, verify serialized request payloads against the shared JSON fixtures in `fixtures/`. - -#### Unit tests - -- **Shared Fixtures Compliance:** Load standard JSON fixtures (track, identify, alias) and assert that your SDK produces the exact same JSON payload for the given inputs. -- Model serialization round-trips -- Identity state transitions (anonymous → identified → reset → re-identify) -- Reserved user ID validation -- Pre-init queue replay (web/mobile) -- Consent state machine (granted/denied/pending/dismissed) -- Session renewal logic -- Storage backends and fallback chain (web) -- Auto-capture URL change detection (web) -- Transport selection (beacon vs fetch) (web) -- Request construction and URL encoding -- Error handling and `onError` callback -- Queue overflow behavior - -#### Integration tests — run against `ghcr.io/altertable-ai/altertable-mock:latest` - -The mock server speaks the full Altertable Product Analytics API. The API key is configured via the `ALTERTABLE_MOCK_API_KEY` environment variable, server listens on port `15001`. - -Use a **dual-mode** approach so tests always run — both locally and in CI — without real credentials: - -**In CI (GitHub Actions):** declare the mock as a service container so it is pre-bound to `localhost:15001` before the test step starts: - -```yaml -services: - altertable: - image: ghcr.io/altertable-ai/altertable-mock:latest - ports: - - 15001:15001 - env: - ALTERTABLE_MOCK_API_KEY: test_pk_abc123 - options: >- - --health-cmd "exit 0" - --health-interval 5s - --health-timeout 3s - --health-retries 3 - --health-start-period 10s -``` - -**Outside CI (local development):** use the language-native Testcontainers library to pull and start the mock automatically before the test suite, store the mapped port in an environment variable (e.g. `ALTERTABLE_MOCK_PORT`), and stop the container via an `at_exit` / teardown hook. Skip this step when the `CI` environment variable is set. - -The test base URL is always `http://localhost:${ALTERTABLE_MOCK_PORT:-15001}`. Point every test client instance at this URL. - -Cover at minimum: - -- one `track` call verifying the response shape -- one `identify` call verifying the response shape -- one `alias` call verifying the response shape -- one call with an invalid API key verifying a `401` error response -- one call with an invalid environment verifying an `environment-not-found` error response - -CI should always run lint + typecheck + unit + integration tests (mock-backed). No test should be skipped due to missing credentials. - -### Phase 14: Example App - -**Web and mobile tiers only.** - -Include a runnable mini-app in the `Examples/` directory (or language-idiomatic equivalent) that demonstrates a complete user journey. This example serves as both a manual test bench and a reference for developers. - -The example must match the user journey and API coverage of the [React reference implementation](https://github.com/altertable-ai/altertable-js/tree/main/examples/example-react/src): - -1. **Multi-step Signup Funnel**: A minimum 3-step form (e.g., Personal Info, Account Setup, Plan Selection). -2. **Event Tracking**: - - Track `Step Viewed` on each step. - - Track transition events (e.g., `Personal Info Completed`, `Account Setup Completed`). - - Track interaction events (e.g., `Plan Selected`, `Terms Agreement Changed`). -3. **Identity Management**: - - On the final step, call `identify(user_id, traits)` with the collected information. - - Track `Form Submitted` immediately after identification. -4. **State Persistence**: Verify that the SDK maintains state across step transitions. - -For mobile SDKs, this should be a simple SwiftUI (iOS) or Jetpack Compose (Android) app. For web-framework wrappers, it should be a minimal project using that framework. - -### Packaging requirements - -1. README must include examples for all endpoints (`track`, `identify`, `alias`, `page`). -2. For web tier: export `TrackingConsent` constants for consumer use. - -## Acceptance Checklist - -- [ ] `track`, `identify`, `alias` endpoints implemented -- [ ] Identity model (anonymous → identified → alias → reset) correct per tier -- [ ] Session management works with TTL renewal (web/mobile) -- [ ] Storage persistence with fallback chain (web/mobile) -- [ ] Tracking consent state machine (web/mobile) -- [ ] Pre-init queue with runtime context capture (web/mobile) -- [ ] Auto-capture with cleanup (web) -- [ ] Typed errors with `onError` hook -- [ ] Reserved user ID validation -- [ ] Tests cover all tier-relevant behavior (mock-backed integration tests run in both CI and local dev) -- [ ] Runnable example app matching the React reference journey (web/mobile) -- [ ] Package is publish-ready -- [ ] MIT license and docs present diff --git a/specs/product-analytics/TEST_PLAN.md b/specs/product-analytics/TEST_PLAN.md deleted file mode 100644 index 007647b..0000000 --- a/specs/product-analytics/TEST_PLAN.md +++ /dev/null @@ -1,110 +0,0 @@ -# Standardized Compliance Test Plan - -This document outlines the mandatory test scenarios for all Altertable Product Analytics SDKs. The goal is to ensure consistent behavior across languages and platforms, particularly for edge cases like identity management and queue handling. - -Every SDK must implement these scenarios in its test suite. While implementation details (mocks, syntax) will vary by language, the **inputs** and **expected outcomes** must match this specification. - -## 1. Identity Management - -### Scenario: Identify (First Call) -- **Input:** Call `identify(userId: "user_123")` when no user is currently identified. -- **Expectation:** - - `userId` is stored in persistent storage. - - Subsequent events include `userId: "user_123"`. - - Session ID is generated/maintained. - -### Scenario: Identify (Same User) -- **Input:** Call `identify(userId: "user_123")` when `user_123` is already the current user. -- **Expectation:** - - No-op. - - No new session is started. - - No warning logs. - -### Scenario: Identify (New User - Identity Shift) -- **Input:** Call `identify(userId: "user_456")` when `user_123` is currently identified. -- **Expectation:** - - **Log Warning:** SDK should log a warning that the user identity has changed without a `reset()`. - - **Auto-Reset:** The SDK must automatically call `reset()` internally to clear the old session and traits. - - **Update:** The new `userId` ("user_456") is stored. - - **New Session:** A new session ID is generated for the new user. - -### Scenario: Alias (Linking) -- **Input:** Call `alias(newId: "user_123")` when anonymousId is "anon_abc". -- **Expectation:** - - An `alias` event is enqueued with `previousId: "anon_abc"` and `userId: "user_123"`. - - The stored `userId` is updated to "user_123". - -### Scenario: Reset (Logout) -- **Input:** Call `reset()`. -- **Expectation:** - - `userId` is cleared from storage. - - `anonymousId` is regenerated (or preserved depending on config, default: regenerate). - - Session ID is cleared/regenerated. - - Traits are cleared. - -## 2. Event Tracking - -### Scenario: Basic Track -- **Input:** Call `track(event: "Button Clicked", properties: { "color": "blue" })`. -- **Expectation:** - - Event is enqueued. - - Payload includes: - - `event`: "Button Clicked" - - `properties`: { "color": "blue" } - - `userId`: (current user ID or null) - - `anonymousId`: (current anonymous ID) - - `timestamp`: (ISO 8601) - - `context`: (library info, os, device) - -### Scenario: Track with NIL/Null Properties -- **Input:** Call `track(event: "Viewed", properties: null)`. -- **Expectation:** - - Event is enqueued. - - `properties` defaults to `{}` (empty object) in the payload, or is omitted if the spec allows, but must not crash. - -## 3. Queue & Batching - -### Scenario: Offline Queueing -- **Input:** - 1. Disconnect network (mock). - 2. Call `track("Event A")`. - 3. Call `track("Event B")`. -- **Expectation:** - - Events are stored locally (disk/memory). - - No network requests are attempted immediately (or they fail gracefully). - - `queueSize` increases. - -### Scenario: Batch Flush -- **Input:** - 1. Queue contains 5 events. - 2. Network is restored. - 3. Flush triggered (manual or auto-timer). -- **Expectation:** - - Events are batched into a single (or few) HTTP requests. - - On 200 OK: Events are removed from the queue. - - On 5xx Error: Events are kept in the queue for retry (with backoff). - - On 4xx Error (e.g., 400 Bad Request): Events are dropped (to prevent infinite loops) and an error is logged. - -## 4. Configuration - -### Scenario: Disable Tracking -- **Input:** - 1. Initialize with `enabled: false`. - 2. Call `track("Event A")`. -- **Expectation:** - - API call returns immediately. - - No event is enqueued. - - No network activity. - -## 5. System Context - -### Scenario: Automatic Context -- **Input:** Any `track` call. -- **Expectation:** - - Payload `context` object is automatically populated with: - - `library.name` (e.g., "altertable-swift") - - `library.version` - - `os.name` - - `os.version` - - `device.manufacturer` (if available) - - `device.model` (if available) diff --git a/specs/product-analytics/fixtures/README.md b/specs/product-analytics/fixtures/README.md deleted file mode 100644 index b252274..0000000 --- a/specs/product-analytics/fixtures/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Shared Test Fixtures - -These JSON files define the canonical expected output for standard SDK operations. All Altertable Product Analytics SDKs should include tests that load these fixtures, execute the described input, and verify the resulting payload matches the output exactly. - -## How to Use - -1. **Load the Fixture:** Parse the JSON file in your test suite. -2. **Execute:** Call your SDK method using the `input` parameters. -3. **Mock Context:** Ensure your test environment uses the fixed values (e.g., `timestamp`, `anonymous_id`, `$lib_version`) shown in the `output` to match the fixture. -4. **Assert:** Compare the serialized JSON payload of your SDK's request against the `output.payload`. - -## Fixture Index - -| File | Description | -| :--- | :--- | -| `identify_basic.json` | Standard identify call with traits. | -| `track_basic.json` | Basic track event with properties. | -| `track_null_properties.json` | Verifies that `null` input properties result in an empty object `{}`. | -| `alias_basic.json` | Standard alias call linking IDs. | diff --git a/specs/product-analytics/fixtures/alias_basic.json b/specs/product-analytics/fixtures/alias_basic.json deleted file mode 100644 index 4511cf4..0000000 --- a/specs/product-analytics/fixtures/alias_basic.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "description": "Basic alias call linking anonymous ID to new user ID", - "input": { - "new_user_id": "user_456", - "distinct_id": "anon_xyz789" - }, - "output": { - "payload": { - "distinct_id": "anon_xyz789", - "new_user_id": "user_456", - "timestamp": "2023-01-01T12:00:00.000Z" - } - } -} diff --git a/specs/product-analytics/fixtures/identify_basic.json b/specs/product-analytics/fixtures/identify_basic.json deleted file mode 100644 index 0b19610..0000000 --- a/specs/product-analytics/fixtures/identify_basic.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "description": "Standard identify call with traits and no active session", - "input": { - "distinct_id": "user_123", - "traits": { - "email": "user@example.com", - "plan": "pro" - } - }, - "output": { - "payload": { - "distinct_id": "user_123", - "traits": { - "email": "user@example.com", - "plan": "pro" - }, - "anonymous_id": "anon_abc123", - "timestamp": "2023-01-01T12:00:00.000Z" - } - } -} diff --git a/specs/product-analytics/fixtures/track_basic.json b/specs/product-analytics/fixtures/track_basic.json deleted file mode 100644 index 07ae556..0000000 --- a/specs/product-analytics/fixtures/track_basic.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "description": "Basic track call with properties", - "input": { - "event": "Button Clicked", - "properties": { - "color": "blue", - "size": "large" - }, - "distinct_id": "user_123" - }, - "output": { - "payload": { - "event": "Button Clicked", - "properties": { - "color": "blue", - "size": "large", - "$lib": "altertable-sdk", - "$lib_version": "1.0.0" - }, - "distinct_id": "user_123", - "anonymous_id": "anon_abc123", - "timestamp": "2023-01-01T12:00:00.000Z" - } - } -} diff --git a/specs/product-analytics/fixtures/track_null_properties.json b/specs/product-analytics/fixtures/track_null_properties.json deleted file mode 100644 index d000d9a..0000000 --- a/specs/product-analytics/fixtures/track_null_properties.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "description": "Track call with null properties defaulting to empty object", - "input": { - "event": "Viewed Page", - "properties": null, - "distinct_id": "user_123" - }, - "output": { - "payload": { - "event": "Viewed Page", - "properties": { - "$lib": "altertable-sdk", - "$lib_version": "1.0.0" - }, - "distinct_id": "user_123", - "anonymous_id": "anon_abc123", - "timestamp": "2023-01-01T12:00:00.000Z" - } - } -} diff --git a/specs/rest/SPEC.md b/specs/rest/SPEC.md deleted file mode 100644 index 5080847..0000000 --- a/specs/rest/SPEC.md +++ /dev/null @@ -1,69 +0,0 @@ -# Management REST API Client Specification - -This specification covers the Altertable Management REST API — the programmatic interface for managing Altertable resources such as environments, service accounts, connections, databases, and credentials. - -Unlike the other specs in this repository, it is **opt-in**: see [Scope](#scope) below. - -OpenAPI source of truth: `https://app.altertable.ai/rest/v1/openapi.yaml` (version `v1`). - -## Scope - -> ⚠️ **This spec is not implemented by default.** - -General-purpose Altertable SDKs (Lakehouse, Product Analytics) **must NOT** implement the Management REST API. A `build-*-sdk` run targeting a normal client library should ignore the `rest/` directory entirely. - -This API exists to back operational tooling, specifically: - -- the **Altertable CLI**, and -- the **Altertable Terraform provider**. - -Only implement against this spec when the target project is one of those tools, or a future tool with an explicit need to manage Altertable resources programmatically. - -## Source of Truth - -The hosted OpenAPI document is authoritative for all endpoints, request/response schemas, parameters, and status codes: - -- OpenAPI: `https://app.altertable.ai/rest/v1/openapi.yaml` -- API version: `v1` - -This spec intentionally does **not** restate the endpoint contract. Read the OpenAPI document for the precise surface. Everything below frames how to consume it; the OpenAPI document wins on any discrepancy. - -## Base URL - -``` -https://app.altertable.ai/rest/v1 -``` - -All paths in the OpenAPI document are relative to this base. - -## Authentication - -The Management REST API uses **HTTP Bearer authentication** with a management API key. Every request carries: - -``` -Authorization: Bearer atm_... -``` - -Management API keys are prefixed with `atm_`. - -> **Note:** This differs from the Lakehouse API, which uses HTTP Basic Auth (see [`../lakehouse/SPEC.md`](../lakehouse/SPEC.md) § Phase 6: Authentication). Do not conflate the two schemes — a Lakehouse Basic Auth credential is **not** valid here, and an `atm_` management key is **not** valid against the Lakehouse API. - -Implementation requirements: - -- The API key must never appear in logs, error messages, or debug output. -- Resolve the key from explicit configuration first. Consuming tools (CLI, Terraform provider) define their own credential discovery — e.g. config file, environment variable, or provider block — and are responsible for documenting it. - -## Resource Surface (non-normative) - -The following is an **illustrative orientation only** — the OpenAPI document is authoritative and may add or change resources not reflected here. Use it to understand the shape of the API, not as a contract. - -- **Authentication** — `GET /whoami`: identify the authenticated principal and organization. -- **Environments** — retrieve and create environments (addressable by UUID or slug). -- **Service Accounts** — create and delete service accounts. -- **Connections** — CRUD over an environment's data connections. -- **Databases** — CRUD over an environment's databases. -- **Credentials** — create, retrieve, and revoke per-environment credentials for both users and service accounts. - -## Transport and Reliability - -For HTTP client performance best practices — keep-alive, timeout defaults, and language-specific HTTP client recommendations — read and follow the [HTTP transport spec](../http/SPEC.md).