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.
| Aspect | B2C | B2B |
|---|---|---|
| Primary entity | User | Account |
| Decision maker | Same as user | Different from user |
| Success metric | User engagement | Account health |
| Churn unit | User | Account (with all its users) |
| Value | Individual | Collective |
Users
Individual humans using your product. User traits are stable, enduring attributes set via identify().
Required traits:
| Trait | Description |
|---|---|
user_id | Stable unique identifier |
email | For identity resolution across systems |
account_id | Which account they belong to |
role | Their role in the account |
Recommended traits:
| Trait | Description |
|---|---|
created_at | When they signed up |
last_login | Most recent login |
subscription_level | Current subscription tier |
Accounts
Organizations, companies, or workspaces. Account traits are set via group().
Required traits:
| Trait | Description |
|---|---|
account_id | Stable unique identifier |
name | Account name (without this, accounts display as numeric IDs) |
plan | Current pricing plan |
Recommended traits:
| Trait | Description |
|---|---|
created_at | When the account was created |
mrr | Monthly recurring revenue (in cents) |
employee_count | Company size for segmentation |
industry | Vertical |
number_of_users | Total users in the account |
billing_cycle | monthly or annual |
status | trial, 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.
| Data | Belongs on | Set via | Why |
|---|---|---|---|
| Email, name, role | User | identify() | Individual attributes |
| Plan, MRR, industry | Account | group() | Organization attributes |
| Feature usage counts | Account | group() (snapshot) | Account-level metrics |
| Last login | User | identify() | Individual activity |
| Employee count | Account | group() | 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
| Trigger | Action |
|---|---|
| User login | Call group() for every level the user has access to |
| Context switch | Call group() when user navigates to a different workspace or project |
| Entity creation | Call group() when a new workspace or project is created |
| Trait change | Call 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.