- schema: add crash/report.type conditionals (allOf if/then) so a crash/anr/native_crash/hang/abnormal_exit manifest requires `crash` and a `manual` manifest forbids it, matching ValidateManifest. - service: reject uploads where X-Profile-Id and manifest.report.profile_id are both present but differ (new ErrProfileMismatch, mapped to 400 profile_mismatch) instead of silently preferring the header; single-source and matching cases unchanged. Adds service tests for mismatch, match, and header-only attribution. - schema: require manifest.json as the first archive.entries element via prefixItems (contains retained for validators without prefixItems support). - schema: document that maxLength is a character-count bound while the server enforces UTF-8 byte length, via a top-level note and per-field notes on the free-text device_summary and crash fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e3QjbPo96ed9Mn2qRiUkh
393 lines
10 KiB
JSON
393 lines
10 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://silo-server.dev/schemas/client-diagnostics/v1/manifest.schema.json",
|
|
"title": "Silo Client Diagnostics Manifest v1",
|
|
"description": "Canonical manifest contract for a client diagnostics bundle. STRING LENGTHS: every `maxLength` in this schema is a character-count sanity bound. The server enforces the matching limit as a UTF-8 *byte* length (Go `len(string)`), so a value near a cap that contains multi-byte characters (CJK, emoji, accented text) can satisfy `maxLength` here yet be rejected at ingest. Treat each numeric cap as a byte budget for non-ASCII text. JSON Schema cannot express byte length, so this divergence is intentional and documented rather than encoded.",
|
|
"type": "object",
|
|
"required": [
|
|
"schema_version",
|
|
"report",
|
|
"destination",
|
|
"consent",
|
|
"device_summary",
|
|
"playback_session_ids",
|
|
"log_summary",
|
|
"archive"
|
|
],
|
|
"properties": {
|
|
"schema_version": {
|
|
"type": "integer",
|
|
"const": 1
|
|
},
|
|
"report": {
|
|
"type": "object",
|
|
"required": [
|
|
"type",
|
|
"captured_at",
|
|
"capture_session_id",
|
|
"app_version",
|
|
"app_build",
|
|
"platform",
|
|
"os_version"
|
|
],
|
|
"properties": {
|
|
"type": {
|
|
"type": "string",
|
|
"enum": [
|
|
"crash",
|
|
"anr",
|
|
"native_crash",
|
|
"hang",
|
|
"abnormal_exit",
|
|
"manual"
|
|
],
|
|
"maxLength": 32
|
|
},
|
|
"captured_at": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"maxLength": 64
|
|
},
|
|
"capture_session_id": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 128
|
|
},
|
|
"app_version": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 64
|
|
},
|
|
"app_build": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 64
|
|
},
|
|
"platform": {
|
|
"type": "string",
|
|
"enum": [
|
|
"android",
|
|
"android-tv",
|
|
"ios",
|
|
"tvos"
|
|
],
|
|
"maxLength": 32
|
|
},
|
|
"os_version": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 128
|
|
},
|
|
"profile_id": {
|
|
"type": "string",
|
|
"maxLength": 128
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"destination": {
|
|
"type": "object",
|
|
"required": [
|
|
"server_instance_id"
|
|
],
|
|
"properties": {
|
|
"server_instance_id": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 128
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"consent": {
|
|
"type": "object",
|
|
"required": [
|
|
"mode",
|
|
"notice_version"
|
|
],
|
|
"properties": {
|
|
"mode": {
|
|
"type": "string",
|
|
"enum": [
|
|
"prompt",
|
|
"always",
|
|
"manual"
|
|
],
|
|
"maxLength": 32
|
|
},
|
|
"notice_version": {
|
|
"type": "integer",
|
|
"minimum": 1
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"crash": {
|
|
"type": "object",
|
|
"required": [
|
|
"summary",
|
|
"source",
|
|
"provenance",
|
|
"occurred_at"
|
|
],
|
|
"properties": {
|
|
"summary": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Free-text crash summary; may contain non-ASCII. maxLength counts characters — the server enforces 8192 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 8192
|
|
},
|
|
"stack_excerpt": {
|
|
"type": "string",
|
|
"description": "Free-text stack excerpt; may contain non-ASCII. maxLength counts characters — the server enforces 8192 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 8192
|
|
},
|
|
"thread": {
|
|
"type": "string",
|
|
"description": "Thread name; may contain non-ASCII. maxLength counts characters — the server enforces 128 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 128
|
|
},
|
|
"foreground": {
|
|
"type": "boolean"
|
|
},
|
|
"source": {
|
|
"type": "string",
|
|
"enum": [
|
|
"ueh",
|
|
"exit_info",
|
|
"metrickit",
|
|
"exit_sentinel"
|
|
],
|
|
"maxLength": 32
|
|
},
|
|
"provenance": {
|
|
"type": "string",
|
|
"enum": [
|
|
"pre_failure",
|
|
"post_restart",
|
|
"metric_reporting_period"
|
|
],
|
|
"maxLength": 32
|
|
},
|
|
"occurred_at": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"maxLength": 64
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"device_summary": {
|
|
"type": "object",
|
|
"required": [
|
|
"manufacturer",
|
|
"model",
|
|
"os",
|
|
"form_factor"
|
|
],
|
|
"properties": {
|
|
"manufacturer": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Device manufacturer; may contain non-ASCII. maxLength counts characters — the server enforces 128 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 128
|
|
},
|
|
"model": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Device model; may contain non-ASCII (e.g. CJK). maxLength counts characters — the server enforces 128 UTF-8 bytes, so a 128-character multi-byte value can pass this schema yet be rejected at ingest (see the schema-level string-length note).",
|
|
"maxLength": 128
|
|
},
|
|
"os": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "OS name/version; may contain non-ASCII. maxLength counts characters — the server enforces 128 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 128
|
|
},
|
|
"form_factor": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Device form factor; may contain non-ASCII. maxLength counts characters — the server enforces 64 UTF-8 bytes (see the schema-level string-length note).",
|
|
"maxLength": 64
|
|
}
|
|
},
|
|
"additionalProperties": {
|
|
"type": "string",
|
|
"maxLength": 256
|
|
}
|
|
},
|
|
"playback_session_ids": {
|
|
"type": "array",
|
|
"maxItems": 20,
|
|
"items": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 128
|
|
}
|
|
},
|
|
"log_summary": {
|
|
"type": "object",
|
|
"required": [
|
|
"lines",
|
|
"bytes_gz",
|
|
"dropped_lines",
|
|
"categories",
|
|
"debug_logging"
|
|
],
|
|
"properties": {
|
|
"lines": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"bytes_gz": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"dropped_lines": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"categories": {
|
|
"type": "array",
|
|
"uniqueItems": true,
|
|
"maxItems": 9,
|
|
"items": {
|
|
"type": "string",
|
|
"enum": [
|
|
"playback",
|
|
"focus",
|
|
"network",
|
|
"lifecycle",
|
|
"browse",
|
|
"cast",
|
|
"download",
|
|
"crash",
|
|
"other"
|
|
],
|
|
"maxLength": 32
|
|
}
|
|
},
|
|
"debug_logging": {
|
|
"type": "boolean"
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
},
|
|
"archive": {
|
|
"type": "object",
|
|
"required": [
|
|
"entries",
|
|
"bytes",
|
|
"uncompressed_bytes",
|
|
"sha256"
|
|
],
|
|
"properties": {
|
|
"entries": {
|
|
"type": "array",
|
|
"description": "Tar member names in bundle order. The first entry MUST be `manifest.json`: the server reads and validates the leading member before any other and compares this order against the actual bundle. `prefixItems` pins index 0, while `contains` keeps `manifest.json` required for validators that do not implement `prefixItems`.",
|
|
"uniqueItems": true,
|
|
"minItems": 1,
|
|
"maxItems": 8,
|
|
"prefixItems": [
|
|
{
|
|
"const": "manifest.json"
|
|
}
|
|
],
|
|
"contains": {
|
|
"const": "manifest.json"
|
|
},
|
|
"items": {
|
|
"type": "string",
|
|
"enum": [
|
|
"manifest.json",
|
|
"device.json",
|
|
"logs.jsonl",
|
|
"crash/summary.json",
|
|
"crash/stack.txt",
|
|
"crash/tombstone.pb",
|
|
"crash/metrickit.json",
|
|
"breadcrumbs.jsonl"
|
|
],
|
|
"maxLength": 64
|
|
}
|
|
},
|
|
"bytes": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"uncompressed_bytes": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"sha256": {
|
|
"type": "string",
|
|
"pattern": "^[A-Fa-f0-9]{64}$",
|
|
"maxLength": 64
|
|
}
|
|
},
|
|
"additionalProperties": true
|
|
}
|
|
},
|
|
"additionalProperties": true,
|
|
"allOf": [
|
|
{
|
|
"$comment": "Event reports (crash/anr/native_crash/hang/abnormal_exit) must carry the crash object; mirrors ValidateManifest in internal/diagnostics/contract/contract.go.",
|
|
"if": {
|
|
"properties": {
|
|
"report": {
|
|
"properties": {
|
|
"type": {
|
|
"enum": [
|
|
"crash",
|
|
"anr",
|
|
"native_crash",
|
|
"hang",
|
|
"abnormal_exit"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"type"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"report"
|
|
]
|
|
},
|
|
"then": {
|
|
"required": [
|
|
"crash"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"$comment": "Manual captures must not carry a crash object.",
|
|
"if": {
|
|
"properties": {
|
|
"report": {
|
|
"properties": {
|
|
"type": {
|
|
"const": "manual"
|
|
}
|
|
},
|
|
"required": [
|
|
"type"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"report"
|
|
]
|
|
},
|
|
"then": {
|
|
"not": {
|
|
"required": [
|
|
"crash"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|