10 minute Forge analytics guide

Get Forge analytics working in 10 minutes - everything else can wait

What You'll Build

A cheap and cheerful analytics implementation that:

  • ✅ Tracks user actions without violating Forge privacy policies
  • ✅ Works with any analytics provider (Accoil, Segment, etc.)
  • ✅ Can be tested locally without sending real data
  • Protects your "Runs on Atlassian" status by preventing End User Data (EUD) egress
  • ✅ Limits PII transmission to keep you compliant

Note: This is the minimal viable implementation. For production apps with high volume or complex requirements, see our Complete Analytics Setup for the full queue-based architecture we recommend.

Prerequisites

  • A working Forge app
  • An analytics API key (we'll use Accoil in examples)
  • 10 minutes

The Essential Pattern

Frontend Events → Backend Resolver → Analytics Provider
                                   ↑
                      Backend Events

Key Rule: Frontend NEVER talks directly to analytics. Everything goes through your backend to prevent End User Data (EUD) transmission, which would jeopardize your "Runs on Atlassian" vendor status.

Step 1: Update Your Manifest (1 minutes)

Add these permissions to manifest.yml:

permissions:
  external:
    fetch:
      backend:
        - address: "in.accoil.com"  # or your analytics provider
          category: analytics
          inScopeEUD: false        # CRITICAL: Protects "Runs on Atlassian" status

Step 2: Create the Backend Dispatcher (3 minutes)

Create src/analytics/dispatcher.js:

import { fetch } from '@forge/api';

// Main function that handles all analytics calls
export const sendAnalytics = async (eventName, userId) => {
  // Skip in debug mode
  if (process.env.ANALYTICS_DEBUG === 'true') {
    console.log(`[Analytics Debug] Would send: ${eventName} for user ${userId}`);
    return;
  }

  // Send to your analytics provider
  await fetch('https://in.accoil.com/v1/events', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      event: eventName,
      user_id: userId,
      api_key: process.env.ANALYTICS_API_KEY,
      timestamp: Date.now()
    })
  });
};

Step 3: Add the Resolver (2 minutes)

In your main resolver file (usually src/index.js):

import { sendAnalytics } from './analytics/dispatcher';

resolver.define('track-event', async ({ payload, context }) => {
  const userId = context.accountId;  // Atlassian user ID
  await sendAnalytics(payload.event, userId);
});

Step 4: Track Events (2 minutes)

Frontend Tracking

Create static/spa/src/analytics.js:

import { invoke } from '@forge/bridge';

export const track = (eventName) => {
  invoke('track-event', { event: eventName });
};

Use it in your React components:

import { track } from './analytics';

// In your component
const handleSearch = (query) => {
  if (query.length > 2) {
    track('Search Performed');
  }
  // ... rest of your search logic
};

Backend Tracking

Track directly from your resolvers for server-side events:

// In your main resolver (src/index.js)
import { sendAnalytics } from './analytics/dispatcher';

resolver.define('create-todo', async ({ payload, context }) => {
  // Your business logic
  const todo = await createTodo(payload);
  
  // Track the backend event
  const userId = context.accountId;
  await sendAnalytics('Todo Created', userId);
  
  return todo;
});

Step 5: Configure and Test (2 minute)

Set your API key:

forge variables set ANALYTICS_API_KEY your_key_here

Enable debug mode for testing:

forge variables set ANALYTICS_DEBUG true

Deploy and test:

forge deploy
forge logs --tail

You should see:

[Analytics Debug] Would send: Search Performed for user 123456789
[Analytics Debug] Would send: Todo Created for user 123456789

That's It! 🎉

You now have working, analytics tracking both frontend interactions and backend operations. Everything else is optimization.

What's Next?

Need to track more events? Focus on meaningful business events:

  • Frontend: Call track('Search Performed') or track('Filter Applied')
  • Backend: Call sendAnalytics('Todo Created', userId) or sendAnalytics('User Invited', userId)

Want to reduce costs / GDPR compliance? Use the account ID instead of user ID:

const userId = context.cloudId;  // Instance ID instead of user ID

Ready for production? Turn off debug mode:

forge variables unset ANALYTICS_DEBUG

Common Gotchas

  • Don't send user emails, names, or any PII
  • Don't track from frontend without going through resolver
  • Don't forget to set inScopeEUD: false in manifest
  • Don't track generic interactions like "clicked" or "viewed" - focus on business outcomes


Time to first event: 10 minutes ⏱️