AccoilAccoil Developer Docs
Best Practices

B2B Tracking Patterns

Patterns for tracking users and accounts in B2B SaaS products.

B2B product analytics is fundamentally different from B2C. The primary entity is the account, not the user. The decision maker is often different from the user. Success is measured by account health, not individual engagement. Churn means losing an account with all its users, not a single person.

This page covers the patterns that make B2B tracking work.

The two-entity model

In B2B SaaS, both users and accounts are first-class entities. Every event must be attributable to both.

AspectB2CB2B
Primary entityUserAccount
Decision makerSame as userDifferent from user
Success metricUser engagementAccount health
Churn unitUserAccount (with all its users)
ValueIndividualCollective

Users

Individual humans using your product. User traits are stable, enduring attributes set via identify().

Required traits:

TraitDescription
user_idStable unique identifier
emailFor identity resolution across systems
account_idWhich account they belong to
roleTheir role in the account

Recommended traits:

TraitDescription
created_atWhen they signed up
last_loginMost recent login
subscription_levelCurrent subscription tier

Accounts

Organizations, companies, or workspaces. Account traits are set via group().

Required traits:

TraitDescription
account_idStable unique identifier
nameAccount name (without this, accounts display as numeric IDs)
planCurrent pricing plan

Recommended traits:

TraitDescription
created_atWhen the account was created
mrrMonthly recurring revenue (in cents)
employee_countCompany size for segmentation
industryVertical
number_of_usersTotal users in the account
billing_cyclemonthly or annual
statustrial, paid, canceled

Group calls are mandatory

In B2B, you must call group() to associate users with accounts. This is not optional.

Without group calls:

  • Account-level analysis is impossible
  • Accoil cannot calculate account engagement scores
  • You lose the most important dimension in B2B analytics
  • Segmentation by account traits (plan, MRR, industry) does not work

The identify, group, track flow

Every B2B integration follows this sequence. The order matters.

Step 1: Identify the user

// On signup or login
identify("usr_12345", {
  email: "jane@acme.com",
  name: "Jane Smith",
  role: "admin",
  created_at: "2024-01-15T10:30:00Z"
});

Step 2: Associate with an account

// Immediately after identify
group("acc_67890", {
  name: "Acme Corp",
  plan: "enterprise",
  mrr: 99900,
  industry: "technology",
  employee_count: 150,
  created_at: "2023-06-01T00:00:00Z",
  status: "paid"
});

Step 3: Track actions

// As the user performs actions
track("Report_Created");
track("Integration_Connected");
track("Teammate_Invited");

Accoil track calls

Accoil's track call accepts only the event name -- no properties. Context comes from the user and account traits you set in identify and group calls.

When to re-identify and re-group

You do not need to call identify and group before every track call. Re-call them when:

  • User logs in -- re-establish identity for the session
  • User traits change -- role promotion, name update
  • Account traits change -- plan upgrade, MRR change, status change
  • Snapshot metrics refresh -- daily or hourly scheduled sync of current-state data

See Snapshot Metrics for the scheduled refresh pattern.

User traits vs account traits

The distinction between user traits and account traits is critical. Putting the wrong data on the wrong entity breaks segmentation.

DataBelongs onSet viaWhy
Email, name, roleUseridentify()Individual attributes
Plan, MRR, industryAccountgroup()Organization attributes
Feature usage countsAccountgroup() (snapshot)Account-level metrics
Last loginUseridentify()Individual activity
Employee countAccountgroup()Organization attribute

The rule: If the data changes when a user switches accounts, it is an account trait. If it stays the same regardless of account context, it is a user trait.

Group hierarchy for multi-level products

Many B2B products have more structure than a single account. If your product has workspaces, projects, instances, or other nested entities, you need a group hierarchy.

Example: Account > Workspace > Project

// 1. Account (top level) -- always required
group("acc_456", {
  name: "Acme Corp",
  group_type: "account",
  plan: "enterprise"
});

// 2. Workspace (child of account)
group("ws_789", {
  name: "Engineering",
  group_type: "workspace",
  parent_group_id: "acc_456"
});

// 3. Project (child of workspace)
group("proj_123", {
  name: "Q1 Release",
  group_type: "project",
  parent_group_id: "ws_789"
});

Every group level needs its own group() call. The parent_group_id trait establishes the hierarchy so rollups work. Without group calls at every level, events attributed to sub-account groups will be orphaned.

Attributing events to the right level

Each event should be attributed to the most specific group where it occurred. Accoil uses context.groupId for this:

{
  "type": "track",
  "event": "Task_Completed",
  "userId": "usr_12345",
  "context": {
    "groupId": "proj_123"
  }
}

This event contributes to:

  • Project-level metrics (directly)
  • Workspace-level metrics (via rollup)
  • Account-level metrics (via rollup)

Analytics tools can roll metrics up the hierarchy. They cannot break them down if you only track at higher levels. Always be as specific as possible.

When to call group() for sub-account levels

TriggerAction
User loginCall group() for every level the user has access to
Context switchCall group() when user navigates to a different workspace or project
Entity creationCall group() when a new workspace or project is created
Trait changeCall group() when group properties change

See Group Hierarchy for comprehensive documentation on hierarchical groups.

Multi-account users

Some B2B products allow users to belong to multiple accounts.

Primary account model

The user has one primary account and can access others:

identify("usr_123", {
  email: "jane@acme.com",
  primary_account_id: "acc_456"
});

Context switching model

Track when users switch between accounts:

// User switches to a different account
track("Account_Switched");

// Re-group with the new account context
group("acc_789", {
  name: "Other Corp",
  plan: "pro"
});

The non-negotiable rule

Regardless of which model you use, every event needs account context. Include the current account_id on every event, either through the SDK's group context or explicitly in the payload.

B2B anti-patterns

These are the most common mistakes in B2B tracking:

No account context

Every event without account context is a lost data point. If you track user actions without associating them to an account, account-level analysis is impossible. Always call group().

User-centric analysis only

Do not just count users. Count accounts and users per account. An account with 50 active users out of 200 seats tells a different story than an account with 2 active users out of 3 seats.

Missing collaboration events

In B2B, multiple users within an account means higher stickiness. Track invites, shares, and collaboration actions. These are leading indicators of retention.

No billing signals

Commercial events (trials, upgrades, limit reached) are critical for revenue analysis. Without them, you cannot connect product behavior to commercial outcomes.

Treating all users equally

Roles matter. An admin configuring integrations and a member completing tasks have different analytical weight. Use role as a user trait via identify() to enable role-based segmentation.

Per-user tracking when account-level suffices

If your product analytics are account-level ("Is Acme Corp adopting this feature?"), tracking every individual user inflates costs for no analytical benefit. See Cost Optimization for instance-level tracking patterns that can reduce costs by 90% or more.

On this page