Skip to content

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.

  1. You send a log with tags
  2. The desktop app extracts each key-value pair
  3. New keys appear as filter dropdowns in the filter bar
  4. New values appear as options in those dropdowns
  5. Value counts are shown so you know the distribution

No schema. No config file. No “register your tags” step. Just send them.

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.

Both tags and context accept arbitrary data, but they serve different purposes:

TagsContext
TypeRecord<string, string> (strings only)Record<string, unknown> (any JSON)
PurposeFiltering and groupingDetailed inspection
UIAppear as filter dropdownsShown in expanded log view
IndexedYes (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,
},
});

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 schema
gunsole.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 }

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.

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-checked
gunsole.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" },
});

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 accepted
gunsole.log({ bucket: "app", message: "Event", tags: { anything: "goes" } });

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",
});
ExportDescription
TagEntrySingle-key tag entry type
ReservedTagKeyUnion of reserved tag keys
ValidTagSchemaCompile-time tag schema constraint
TagExample valuesUse case
route/users, /ordersFilter by API endpoint
methodGET, POST, PUTFilter by HTTP method
status200, 404, 500Filter by response status
featureauth, checkout, searchFilter by product area
actionclick, submit, navigateFilter by user action type
serviceapi, worker, schedulerFilter by microservice
envproduction, staging, localFilter by environment
componentHeader, Cart, ModalFilter by React component

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

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.