Tags
Tags are the most powerful filtering mechanism in gunsole. They’re simple key-value pairs attached to log entries, but the magic is in how they surface in the UI.
How it works
Section titled “How it works”- You send a log with tags
- The desktop app extracts each key-value pair
- New keys appear as filter dropdowns in the filter bar
- New values appear as options in those dropdowns
- Value counts are shown so you know the distribution
No schema. No config file. No “register your tags” step. Just send them.
Example
Section titled “Example”gunsole.info({ bucket: "api", message: "Request handled", tags: { route: "/users", method: "GET", status: "200", },});
gunsole.info({ bucket: "api", message: "Request handled", tags: { route: "/orders", method: "POST", status: "201", },});
gunsole.error({ bucket: "api", message: "Request failed", tags: { route: "/orders", method: "POST", status: "500", },});After these three logs, the filter bar shows:
- route dropdown:
/users(1),/orders(2) - method dropdown:
GET(1),POST(2) - status dropdown:
200(1),201(1),500(1)
Select status: 500 to see only the failed request. Select route: /orders + method: POST to see all POST requests to /orders.
Tag filtering is AND across keys, OR within values. For example, action: [login, signup] AND region: [us-east] means: logs where action is login OR signup, AND region is us-east.
Tags vs. context
Section titled “Tags vs. context”Both tags and context accept arbitrary data, but they serve different purposes:
| Tags | Context | |
|---|---|---|
| Type | Record<string, string> (strings only) | Record<string, unknown> (any JSON) |
| Purpose | Filtering and grouping | Detailed inspection |
| UI | Appear as filter dropdowns | Shown in expanded log view |
| Indexed | Yes (fast queries) | No (stored as JSON blob) |
Rule of thumb: If you want to filter by it, use a tag. If you want to inspect it when looking at a specific log, use context.
gunsole.info({ bucket: "api", message: "POST /orders → 201", // Things you filter by tags: { route: "/orders", method: "POST", status: "201", }, // Things you look at when investigating context: { orderId: "ord_abc123", items: 3, total: 149.99, userId: "u_456", latencyMs: 234, },});Tag entries
Section titled “Tag entries”Tags can be passed as an object or as an array of single-key entries (useful for dynamic tag assembly):
// Object form (most common)gunsole.log({ bucket: "app", message: "Event", tags: { region: "us-east", feature: "auth" },});
// Array form — each entry is a single-key object from the tag schemagunsole.log({ bucket: "app", message: "Event", tags: [{ region: "us-east" }, { feature: "auth" }],});The type signature in LogOptions is:
tags?: Partial<Tags> | TagEntry<Tags>[];Where TagEntry<T> produces a union of single-key picks:
type TagEntry<T> = { [K in keyof T]: Pick<T, K> }[keyof T];// For AppTags → { action: string } | { feature: string }Default tags
Section titled “Default tags”Set defaultTags in your client config to automatically attach tags to every log:
const gunsole = createGunsoleClient({ projectId: "my-app", mode: "local", defaultTags: { service: "api-server", region: "us-east-1", version: "1.4.2", },});Every log from this client will have service, region, and version tags. Per-log tags are merged on top — if there’s a conflict, the per-log value wins.
Typed tags
Section titled “Typed tags”Define a tag schema as a generic parameter for compile-time safety and autocomplete:
import { createGunsoleClient } from "@gunsole/web";
type MyTags = { region: string; feature: string; provider: string; action: string;};
const gunsole = createGunsoleClient<MyTags>({ projectId: "my-app", mode: "local", defaultTags: { region: "us-east" }, buckets: ["payments", "auth"] as const,});
// Tags are now type-checkedgunsole.payments("Checkout completed", { tags: { feature: "checkout", provider: "stripe" }, // ✅ valid keys});
gunsole.auth.error("Login failed", { tags: { action: "login", region: "eu-west" }, // ✅ valid keys});
gunsole.info({ bucket: "api", message: "Request", tags: { endpoint: "/users" }, // ❌ TypeScript error: "endpoint" not in MyTags});Works with both log() and typed bucket accessors:
gunsole.log({ bucket: "app", message: "Event", tags: { region: "us-east", feature: "auth" },});
// Error: 'typo' does not exist in type 'Partial<MyTags>'gunsole.log({ bucket: "app", message: "Event", tags: { typo: "value" },});Without a schema
Section titled “Without a schema”Omitting the generic parameter allows any string keys — tags still work, you just don’t get compile-time checking:
const gunsole = createGunsoleClient({ projectId: "my-app", mode: "local",});
// No type error — any key is acceptedgunsole.log({ bucket: "app", message: "Event", tags: { anything: "goes" } });Reserved tag keys
Section titled “Reserved tag keys”Tag keys that collide with internal log entry fields are rejected at compile time via ValidTagSchema:
bucket, message, level, timestamp, userId, sessionId, env, appName, appVersion
// This will cause a type error:type BadTags = { level: string; action: string };// ^^^^^ ❌ "level" is a reserved tag key
const gunsole = createGunsoleClient<BadTags>({ // ^^^^^^^ Type error: ValidTagSchema constraint fails projectId: "my-app", mode: "local",});TypeScript exports
Section titled “TypeScript exports”| Export | Description |
|---|---|
TagEntry | Single-key tag entry type |
ReservedTagKey | Union of reserved tag keys |
ValidTagSchema | Compile-time tag schema constraint |
Good tag patterns
Section titled “Good tag patterns”| Tag | Example values | Use case |
|---|---|---|
route | /users, /orders | Filter by API endpoint |
method | GET, POST, PUT | Filter by HTTP method |
status | 200, 404, 500 | Filter by response status |
feature | auth, checkout, search | Filter by product area |
action | click, submit, navigate | Filter by user action type |
service | api, worker, scheduler | Filter by microservice |
env | production, staging, local | Filter by environment |
component | Header, Cart, Modal | Filter by React component |
How tags are used in the desktop app
Section titled “How tags are used in the desktop app”Tags sent from the SDK are auto-discovered and shown as dynamic filter dropdowns in the log viewer:
- Each unique tag key becomes a multiselect dropdown in the filter bar
- Up to 4 tag filters shown inline; overflow goes to a “More Filters” drawer
- Tag filtering is AND across keys, OR within values
- Tags are stored in a normalized junction table (
tags+log_tag_map) for efficient server-side querying - System tags (e.g.,
log_level) are handled separately with dedicated UI like level toggle buttons
Storage
Section titled “Storage”Tags are stored in a separate tags table with a many-to-many relationship to logs via log_tag_map. This makes tag queries fast — they use indexed JOINs rather than scanning JSON blobs.