---
title: "Scopes & Structured Logs"
description: "Create a scope truth table, remove unnecessary permissions, and implement structured logging with correlation IDs. This lesson teaches you to minimize security surface area while maximizing debuggability in production."
canonical_url: "https://vercel.com/academy/slack-agents/scopes-and-structured-logs"
md_url: "https://vercel.com/academy/slack-agents/scopes-and-structured-logs.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-03-10T13:26:56.224Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Scopes & Structured Logs

# Lock Down Permissions and Make Production Debuggable

Over-scoped bots are security incidents waiting to happen. When your bot has `users:read.email` but never uses it, you're one compromised token away from a data breach. Meanwhile, unstructured `console.log` statements are useless when debugging production issues during a critical customer demo. You need minimal scopes and searchable logs.

## Outcome

Create a scope truth table, trim unused permissions, and implement structured logging that tracks every AI operation with searchable fields.

## Fast Track

1. Generate scope truth table from manifest and code usage
2. Remove unused scopes and reinstall to verify failures
3. Replace all `console.log` with structured `app.logger` calls

## Building on Previous Lessons

We're extending the observability foundation to production scale:

- **From [correlation middleware](./bolt-nitro-middleware-and-logging)**: Correlation middleware forms the base - now we add AI-specific metrics (model, tokens, latency)
- **From [repository flyover](./repository-flyover)**: Context fetching operations get structured logging to track API quotas and rate limits
- **From [AI tools](./ai-tools-and-functions)**: Tool executions log inputs and outputs for debugging AI decision chains
- **From [status communication](./status-communication)**: Status updates become searchable events for measuring user-perceived latency
- **From [error handling](./error-handling-and-resilience)**: Retry attempts, backoff delays, and model fallbacks are tracked for reliability metrics
- **Production reasoning**: Distributed serverless functions need structured logs - correlation IDs are the only way to trace a request across multiple function executions
- **Cost tracking**: Token counts in logs enable chargeback models (per-user/per-channel cost attribution)

## Hands-On Exercise 5.2

Audit your bot's OAuth scopes and implement production-grade structured logging:

**Requirements:**

1. Create a scope truth table mapping each scope to its usage
2. Remove all unused scopes from manifest
3. Test with removed scopes to confirm expected failures
4. Implement structured logging schema for all AI operations
5. Include correlation ID, tokens, latency, and retry info

**Implementation hints:**

- Use `ast-grep` to find all Slack API calls and their required scopes
- Test scope removal by commenting out, reinstalling, and verifying failure
- Add structured fields to every `app.logger` call
- Include both prompt and completion tokens for cost tracking

**Logging schema to implement:**

```typescript
interface StructuredLog {
  correlationId: string;
  operation: 'respondToMessage' | 'toolCall' | 'retry';
  model?: string;
  promptTokens?: number;
  completionTokens?: number;
  retryAttempt?: number;
  rateLimitWaitMs?: number;
  channel?: string;
  thread_ts?: string;
  latencyMs?: number;
  error?: string;
}
```

## Try It

1. **Generate scope truth table:**

   ```bash
   # Find all Slack API calls
   ast-grep --pattern 'client.$METHOD($$$)' server/
   ```

   Create `/slack-agent/SCOPE_AUDIT.md`:

   ```markdown
   # OAuth Scope Audit

   | Scope | Used By | Can Remove? |
   |-------|---------|-------------|
   | app_mentions:read | app-mention.ts:12 - Event subscription | ❌ Required |
   | channels:history | respond-to-message.ts:34 - Thread context | ❌ Required |
   | chat:write | All responses | ❌ Required |
   | groups:history | Private channel support | ✅ Not used |
   | im:history | DM support | ❌ Required |
   | mpim:history | Group DM support | ✅ Not used |
   | users:read | None found | ✅ Remove |
   | users:read.email | None found | ✅ Remove |
   ```

2. **Test scope removal:**

   ```json title="/slack-agent/manifest.json"
   "bot": [
     "app_mentions:read",
     "channels:history",
     "chat:write",
     "im:history"
     // Removed: groups:history, mpim:history, users:read
   ]
   ```

   After reinstall, test in private channel:

   ```
   [ERROR] bolt-app {
     correlationId: 'ev09E5_1234567890.123456',
     error: 'missing_scope',
     required: 'groups:history',
     channel: 'G09D4DG727P'
   } Cannot read private channel history
   ```

3. **Implement structured logging:**
   ```typescript title="/slack-agent/server/lib/ai/respond-to-message.ts" {42-51}
   const response = await openai.chat.completions.create({
     model: 'gpt-4o-mini',
     messages: conversationHistory,
     tools: toolDefinitions,
     stream: true
   });

   app.logger.info({
     ...context.correlation,
     operation: 'respondToMessage',
     model: 'gpt-4o-mini',
     promptTokens: response.usage?.prompt_tokens,
     completionTokens: response.usage?.completion_tokens,
     retryAttempt: retryCount,
     rateLimitWaitMs: rateLimitDelay,
     channel: event.channel,
     thread_ts: event.thread_ts || event.ts,
     latencyMs: Date.now() - startTime
   }, 'AI response generated');
   ```

4. **Verify structured logs in Vercel:**
   ```json
   {
     "level": 30,
     "time": 1733456789123,
     "msg": "AI response generated",
     "correlationId": "ev09E5EDA89M_1234567890.123456",
     "operation": "respondToMessage",
     "model": "gpt-4o-mini",
     "promptTokens": 234,
     "completionTokens": 89,
     "retryAttempt": 0,
     "rateLimitWaitMs": 0,
     "channel": "C09D4DG727P",
     "thread_ts": "1234567890.123456",
     "latencyMs": 1247
   }
   ```

5. **Query logs by correlation ID:**
   ```
   Vercel Logs Filter: correlationId:"ev09E5EDA89M_1234567890.123456"

   Results (4 entries):
   1. [INFO] Processing app_mention event
   2. [INFO] Thread context retrieved (7 messages)
   3. [INFO] Tool call: searchKnowledgeBase
   4. [INFO] AI response generated (1247ms)
   ```

## Troubleshooting

**Missing Scope Errors:**

- Check exact error message for required scope
- Some scopes have dependencies (e.g., `channels:history` needs `channels:read`)
- Private channels need `groups:` scopes even if not explicitly used

**Structured Logging Not Working:**

- Ensure you're using `app.logger` not `console.log`
- Vercel may need `NODE_ENV=production` for proper log levels
- Check log level configuration in production

**Token Counts Missing:**
For streaming responses, collect usage after stream completes:

```typescript
let usage = { prompt_tokens: 0, completion_tokens: 0 };
for await (const chunk of stream) {
  if (chunk.usage) {
    usage = chunk.usage;
  }
}
```

## Commit

```bash
git add -A
git commit -m "feat(security): minimize OAuth scopes and add structured logging

- Create scope audit table with usage mapping
- Remove unused scopes: groups:history, mpim:history, users:read
- Implement structured logging schema with AI metrics
- Add correlation ID, token counts, and latency tracking
- Replace console.log with app.logger throughout"
```

## Done-When

- [x] Scope truth table created in `SCOPE_AUDIT.md`
- [x] Unused scopes removed from manifest
- [x] Bot fails gracefully when accessing removed scopes
- [x] All AI operations log structured data
- [x] Logs queryable by correlation ID in Vercel

## Solution

1. **Example manifest with trimmed scopes:**

```json title="/slack-agent/manifest.json" {6-10}
{
  "oauth_config": {
    "scopes": {
      "bot": [
        "app_mentions:read",
        "channels:history",
        "chat:write",
        "im:history"
      ]
    }
  }
}
```

\*\*Warning: Don't break the bot you just built\*\*

The trimmed scope list above is a **minimal example for a very simple bot**, not a literal copy-paste for your course project.\
Your bot from earlier lessons needs additional scopes like `assistant:write` (assistant threads), `reactions:write` / `reactions:read` (reaction tools and listeners), and `commands` (slash commands), plus the appropriate `channels:*` / `groups:*` / `mpim:*` read scopes for the events you actually subscribed to.\
Use your own `SCOPE_AUDIT.md` and `manifest.json` as the source of truth, and **only remove scopes that your audit marks as unused**.

2. **Structured logging middleware enhancement:**

```typescript title="/slack-agent/server/middleware/correlation.ts" {15-24}
export const correlationMiddleware: Middleware<SlackEventMiddlewareArgs> = async ({
  context,
  body,
  next,
  logger
}) => {
  const event = 'event' in body ? body.event : null;
  const correlationId = event
    ? `${event.type}_${event.event_id}_${Date.now()}`
    : `req_${Date.now()}`;

  const correlation = {
    correlationId,
    event_id: event?.event_id,
    type: event?.type,
    channel: event?.channel,
    ts: event?.ts,
    thread_ts: event?.thread_ts ?? event?.ts,
    user: event?.user
  };

  // Attach correlation to context so all loggers can spread it
  // logger.info({ ...context.correlation, ... }, 'message')
  // logger.error({ ...context.correlation, error }, 'message')
  (context as any).correlation = correlation;

  await next();
};
```

3. **AI operation logging:**

```typescript title="/slack-agent/server/lib/ai/respond-to-message.ts" {28-40}
// Inside your existing respondToMessage implementation (with cost control from 4.5)
const startTime = Date.now();

// ...existing AI request code (generateText / withRetry / cost tracking)...

app.logger.info("AI response completed", {
  ...correlation,
  operation: "respondToMessage",
  // keep any existing cost fields from lesson 4.5 (actualCost, estimatedCost, estimationAccuracy, ...)
  promptTokens: result.usage.promptTokens,
  completionTokens: result.usage.completionTokens,
  channel: event.channel,
  thread_ts: thread_ts || event.ts,
  latencyMs: Date.now() - startTime,
});

// In your catch block:
app.logger.error("AI response failed", {
  ...correlation,
  operation: "respondToMessage",
  error: error instanceof Error ? error.message : String(error),
  channel: event.channel,
  thread_ts: thread_ts || event.ts,
  latencyMs: Date.now() - startTime,
});
```

\*\*Side Quest: AI Cost Attribution System\*\*

## Key Takeaways

- Every OAuth scope is a security risk - only keep what you actually use
- Structured logging with correlation IDs makes debugging possible at scale
- Token tracking is essential for cost management
- Scope audits should be part of your CI/CD pipeline
- Use `app.logger` everywhere - `console.log` is dead to us


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
