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
22 changes: 22 additions & 0 deletions .github/workflows/deploy-telemetry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ jobs:
--env-file deploy/telemetry/.env \
-f deploy/telemetry/docker-compose.yml \
restart caddy
public_host="$(sed -n 's/^TELEMETRY_PUBLIC_HOST=//p' deploy/telemetry/.env | tail -n 1)"
test -n "$public_host"
printf '%s' "$public_host" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$'
for attempt in $(seq 1 30); do
if curl -fsS --connect-timeout 5 --max-time 15 "https://$public_host/readyz" >/dev/null; then
break
fi
test "$attempt" -lt 30
sleep 2
done
for user_agent in ti/deploy-smoke tdc/deploy-smoke; do
response="$(curl -sS --connect-timeout 5 --max-time 15 \
-H 'Content-Type: application/json' \
-H "User-Agent: $user_agent" \
--data '{}' \
-w '\n%{http_code}' \
"https://$public_host/v1/telemetry/batch")"
status="${response##*$'\n'}"
body="${response%$'\n'*}"
test "$status" = "400"
printf '%s' "$body" | grep -F '"message":"schema validation failed"' >/dev/null
done
docker compose \
--env-file deploy/telemetry/.env \
-f deploy/telemetry/docker-compose.yml \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Use `TI_LOGGING=off` to disable logging for one process. Accepted values are `on

Release builds collect minimal command usage and reliability telemetry through the ti-owned ingestion service. Events contain canonical command and explicitly supplied flag names, stable exit and error codes, duration, region, ti version, OS, architecture, install source, and a random installation ID. They never contain flag values, credentials, tokens, SQL text, paths, file contents, command output, API payloads, profile names, or cloud resource IDs.

During the v0.2 migration window, the ingestion backend accepts both current `ti/<version>` and legacy `tdc/<version>` User-Agent values. All other User-Agent prefixes are rejected before payload validation.

Telemetry is disabled by default for development builds and recognized CI environments. To disable it persistently for release builds, create or edit `~/.ti/.preferences`:

```toml
Expand Down
2 changes: 2 additions & 0 deletions docs/telemetry-backend-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ Content-Type: application/json
User-Agent: ti/<version>
```

During the v0.2 migration window, the backend also accepts the legacy `User-Agent: tdc/<version>` value. No other User-Agent prefix is accepted. Deployment smoke tests must verify both accepted prefixes against the public endpoint; a schema-invalid probe must reach payload validation instead of failing header validation.

Request limits:

- Body size: default 64 KiB.
Expand Down
14 changes: 10 additions & 4 deletions internal/telemetrybackend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,16 @@ func validContentType(raw string) bool {
}

func validUserAgent(value string) bool {
return strings.HasPrefix(value, "ti/") &&
len(value) > len("ti/") &&
len(value) <= 128 &&
!strings.ContainsAny(value, "\r\n\t ")
prefixLength := 0
switch {
case strings.HasPrefix(value, "ti/"):
prefixLength = len("ti/")
case strings.HasPrefix(value, "tdc/"):
prefixLength = len("tdc/")
default:
return false
}
return len(value) > prefixLength && len(value) <= 128 && !strings.ContainsAny(value, "\r\n\t ")
}

var errBodyTooLarge = errors.New("request body too large")
Expand Down
26 changes: 26 additions & 0 deletions internal/telemetrybackend/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ func TestServerAcceptsValidBatchWith202BeforeFlush(t *testing.T) {
}
}

func TestServerAcceptsCurrentAndLegacyCLIUserAgents(t *testing.T) {
for _, userAgent := range []string{"ti/0.2.3", "tdc/0.1.7"} {
t.Run(userAgent, func(t *testing.T) {
cfg := testConfig()
batcher := NewBatcher(cfg, nil, discardLogger(), nil)
server := NewServer(cfg, batcher, readinessStub{}, readinessStub{}, discardLogger(), nil)
request := newBatchRequest(validRequestBody())
request.Header.Set("User-Agent", userAgent)
response := httptest.NewRecorder()
server.Handler().ServeHTTP(response, request)
if response.Code != http.StatusAccepted {
t.Fatalf("status = %d, want %d; body=%s", response.Code, http.StatusAccepted, response.Body.String())
}
})
}
}

func TestValidUserAgentRejectsMalformedOrUntrustedValues(t *testing.T) {
tooLong := "ti/" + strings.Repeat("v", 126)
for _, userAgent := range []string{"", "ti/", "tdc/", "curl/8", "ti/0.2.3 debug", "tdc/0.1.7\n", tooLong} {
if validUserAgent(userAgent) {
t.Errorf("validUserAgent(%q) = true", userAgent)
}
}
}

func TestServerRejectsInvalidRequestsWithGenericErrors(t *testing.T) {
cfg := testConfig()
cfg.MaxBodyBytes = int64(len(validRequestBody()))
Expand Down