AccoilAccoil Developer Docs
Atlassian Forge

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

On this page