Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ea71524
heatbeat mvp + unit tests
ak--47 Jun 14, 2025
d32c364
docs!
ak--47 Jun 14, 2025
123e8d3
example + stub for snippet
ak--47 Jun 14, 2025
149aa50
linters gotta lint
ak--47 Jun 14, 2025
1a9f9db
remove unused self
ak--47 Jun 14, 2025
a95c613
vscode testing gui
ak--47 Jun 14, 2025
5852cd1
final test cleanup
ak--47 Jun 14, 2025
bfde92f
vscode rec extension for mocha
ak--47 Jun 14, 2025
874e356
auto $duration + $hits tracking + int tests
ak--47 Jun 14, 2025
ad372ce
$hits to $heartbeats; inherit debug: true
ak--47 Jun 14, 2025
b0e6872
fix old tests
ak--47 Jun 15, 2025
204ef85
spelling
ak--47 Jun 15, 2025
42b2e37
central logger; contentId prefix; simple docs
ak--47 Jun 15, 2025
9f479b3
build + fix integration tests
ak--47 Jun 15, 2025
ba012a7
fix docs + make concise
ak--47 Jun 15, 2025
8da4c3d
final proofreading
ak--47 Jun 16, 2025
e54fa42
greatly simplify API
ak--47 Jun 18, 2025
d3926bd
unit + int tests
ak--47 Jun 18, 2025
7fd875d
bundle
ak--47 Jun 18, 2025
cc92eae
trailing comma.
ak--47 Jun 18, 2025
e47890b
remove heartbeat persistence
ak--47 Jul 8, 2025
bbc72ec
greatly simplify mock
ak--47 Jul 8, 2025
db0452b
.start() / .stop() API
ak--47 Aug 6, 2025
8c4c450
logging
ak--47 Aug 6, 2025
1b58a68
no large arrays; stop() shouldn't forceFlush
ak--47 Aug 6, 2025
a57173e
namespace hb tests
ak--47 Aug 7, 2025
bd6cace
last known #
ak--47 Aug 7, 2025
59d37c5
obvious constants; comment cleanup
ak--47 Aug 7, 2025
3272bc6
defensive try catch
ak--47 Aug 7, 2025
bfd6a48
prefer plan objects to map/sets
ak--47 Aug 7, 2025
19d19be
remove comments
ak--47 Aug 7, 2025
d296a9d
remove visibilitychange; reset log counter
ak--47 Aug 7, 2025
0f93880
clearer consistent logs
ak--47 Aug 7, 2025
bcb1343
Merge branch 'mixpanel:master' into heartbeat-ak
ak--47 Aug 24, 2025
24bd520
fix CI 429s
ak--47 Aug 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,28 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-
${{ runner.os }}-node-

# Configure npm for better reliability
- name: Configure npm
run: |
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set maxsockets 1

- run: npm ci
- run: npm test
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ tunnel.log
*.ps1
*.bundle.js
.DS_Store
examples/heartbeat-demo
8 changes: 8 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"require": ["babel-core/register"],
"spec": ["tests/unit/*.js"],
"ignore": ["tests/unit/test-utils/**/*.js"],
"timeout": 5000,
"ui": "bdd",
"recursive": false
}
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"hbenl.vscode-mocha-test-adapter"
]
}
35 changes: 35 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"mochaExplorer.files": "tests/unit/*.js",
"mochaExplorer.require": ["babel-core/register"],
"mochaExplorer.env": {
"BABEL_ENV": "test"
},
"mochaExplorer.timeout": 5000,
"mochaExplorer.ui": "bdd",
"mochaExplorer.mochaPath": "./node_modules/mocha",
"mochaExplorer.logpanel": true,
"mochaExplorer.autoload": true,
"testExplorer.codeLens": true,
"testExplorer.gutterDecoration": true,
"testExplorer.onStart": "retire",
"testExplorer.onReload": "retire",
"cSpell.words": [
"Ashkenas",
"avocat",
"copypasta",
"esque",
"FLUSHON",
"mainsite",
"mixpanelinit",
"mpap",
"mphb",
"mphbf",
"mplib",
"mpso",
"mpus",
"optout",
"sendbeacon",
"superproperties",
"superprops"
]
}
167 changes: 167 additions & 0 deletions doc/readme.io/javascript-full-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,173 @@ var has_opted_out = mixpanel.has_opted_out_tracking();
| <span class="mp-arg-type">boolean</span> | current opt-out status |


___
## mixpanel.heartbeat
Client-side aggregation for streaming analytics events like video watch time, podcast listen time, or other continuous interactions. `mixpanel.heartbeat()` is safe to be called in a loop without exploding your event counts.

Heartbeat produces a single event which represents many heartbeats; the event which summarizes all the heartbeats is sent when the user stops sending heartbeats for a configurable timeout period (default 30 seconds) or when the page unloads.

**Note**: Heartbeat data is session-scoped and does not persist across page refreshes. All pending heartbeat events are automatically flushed when the page unloads.

Each summary event automatically tracks:
- `$duration`: Seconds from first to last heartbeat call
- `$heartbeats`: Number of heartbeat calls made
- `$contentId`: The contentId parameter

### Basic Usage:
```javascript
mixpanel.heartbeat('video_watch', 'video_123');
mixpanel.heartbeat('video_watch', 'video_123'); // 10 seconds later
mixpanel.heartbeat('video_watch', 'video_123'); // 30 seconds later
// After 30 seconds of inactivity, the event is flushed:
// {event: 'video_watch', properties: {$contentId: 'video_123', $duration: 40, $heartbeats: 3}}
```

You can also pass additional properties, and options to be aggregated with each heartbeat call. Properties are merged intelligently by type:
- Numbers take the latest value
- Strings take the latest value
- Objects are merged (latest overwrites)
- Arrays have elements appended

### Examples:

```javascript
// Force immediate flush
mixpanel.heartbeat('podcast_listen', 'episode_123', { platform: 'mobile' }, { forceFlush: true });

// Custom timeout (60 seconds)
mixpanel.heartbeat('video_watch', 'video_123', { quality: 'HD' }, { timeout: 60000 });

// Property aggregation
mixpanel.heartbeat('video_watch', 'video_123', {
currentTime: 30,
interactions: ['play'],
language: 'en'
});

mixpanel.heartbeat('video_watch', 'video_123', {
currentTime: 45, // latest value: {currentTime: 45}
interactions: ['pause'], // appended: ['play', 'pause']
language: 'fr' // replaced: {language: 'fr'}
});
```

### Auto-Flush Behavior:
Events are automatically flushed when:
- **Time limit reached**: No activity for 30 seconds (or custom timeout)
- **Page unload**: Browser navigation or tab close (uses sendBeacon for reliability)

**Session Scope**: All heartbeat data is stored in memory only and is lost when the page refreshes or navigates away. This design ensures reliable data transmission without cross-page persistence complexity.


| Argument | Type | Description |
| ------------- | ------------- | ----- |
| **event_name** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | The name of the event to track |
| **content_id** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | Unique identifier for the content being tracked |
| **properties** | <span class="mp-arg-type">Object</span></br></span><span class="mp-arg-optional">optional</span> | Properties to aggregate with existing data |
| **options** | <span class="mp-arg-type">Object</span></br></span><span class="mp-arg-optional">optional</span> | Configuration options |
| **options.timeout** | <span class="mp-arg-type">Number</span></br></span><span class="mp-arg-optional">optional</span> | Timeout in milliseconds (default 30000) |
| **options.forceFlush** | <span class="mp-arg-type">Boolean</span></br></span><span class="mp-arg-optional">optional</span> | Force immediate flush after aggregation |



___
## mixpanel.heartbeat.start
Start a managed heartbeat that automatically sends heartbeat calls at regular intervals. This is ideal for tracking continuous activities like video watching or audio playback where you want automated tracking without manual heartbeat() calls.

**Important**: You cannot mix `mixpanel.heartbeat()` calls with `mixpanel.heartbeat.start()` for the same event and content ID. Use one approach or the other.

### Basic Usage:
```javascript
// Start managed heartbeat with default 5-second interval
mixpanel.heartbeat.start('video_watch', 'video_123');

// Stop the managed heartbeat when user stops watching
mixpanel.heartbeat.stop('video_watch', 'video_123');
```

### Custom Interval:
```javascript
// Start with custom 10-second interval
mixpanel.heartbeat.start('podcast_listen', 'episode_456',
{ platform: 'mobile' },
{ interval: 10000 }
);
```

### Property Aggregation:
Properties passed to `heartbeat.start()` are sent with each interval heartbeat and aggregated the same way as manual heartbeat calls:

```javascript
mixpanel.heartbeat.start('game_session', 'level_1', {
score: 100, // Numbers use latest value each interval
level: 'easy', // Strings use latest value
powerups: ['speed'] // Arrays have elements appended
});

// After multiple intervals, properties are aggregated:
// {score: 100, level: 'easy', powerups: ['speed', 'speed', 'speed']}
```

### Auto-Management:
- Automatically calls internal heartbeat at specified intervals (default 5 seconds)
- Each interval call aggregates the provided properties
- Includes standard automatic properties: `$duration`, `$heartbeats`, `$contentId`
- Must be stopped with `mixpanel.heartbeat.stop()` to flush final event

**Session Scope**: Like manual heartbeat calls, managed heartbeats are session-scoped and do not persist across page refreshes.


| Argument | Type | Description |
| ------------- | ------------- | ----- |
| **event_name** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | The name of the event to track |
| **content_id** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | Unique identifier for the content being tracked |
| **properties** | <span class="mp-arg-type">Object</span></br></span><span class="mp-arg-optional">optional</span> | Properties to include with each heartbeat interval |
| **options** | <span class="mp-arg-type">Object</span></br></span><span class="mp-arg-optional">optional</span> | Configuration options |
| **options.interval** | <span class="mp-arg-type">Number</span></br></span><span class="mp-arg-optional">optional</span> | Interval in milliseconds between heartbeats (default 5000) |


___
## mixpanel.heartbeat.stop
Stop a managed heartbeat started with `mixpanel.heartbeat.start()` and immediately flush the aggregated event data.

### Basic Usage:
```javascript
// Start managed tracking
mixpanel.heartbeat.start('video_watch', 'video_123', { quality: 'HD' });

// Stop and flush when user stops watching (e.g., pause, close)
mixpanel.heartbeat.stop('video_watch', 'video_123');
```

### Immediate Flush:
When `heartbeat.stop()` is called:
1. The interval timer is immediately cleared (no more automatic heartbeats)
2. Any accumulated heartbeat data is immediately flushed as a track event
3. The event includes all aggregated properties and automatic properties

### Multiple Concurrent Heartbeats:
You can run multiple managed heartbeats simultaneously:

```javascript
// Start multiple different content tracking
mixpanel.heartbeat.start('video_watch', 'video_123');
mixpanel.heartbeat.start('podcast_listen', 'episode_456');

// Stop them independently
mixpanel.heartbeat.stop('video_watch', 'video_123'); // Flushes video data
mixpanel.heartbeat.stop('podcast_listen', 'episode_456'); // Flushes podcast data
```

**Note**: Calling `stop()` on a non-existent heartbeat is safe and will not produce errors.


| Argument | Type | Description |
| ------------- | ------------- | ----- |
| **event_name** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | The name of the event to stop tracking |
| **content_id** | <span class="mp-arg-type">String</span></br></span><span class="mp-arg-required">required</span> | Unique identifier for the content to stop tracking |


___
## mixpanel.identify
Identify a user with a unique ID to track user activity across devices, tie a user to their events, and create a user profile. If you never call this method, unique visitors are tracked using a UUID generated the first time they visit the site.
Expand Down
Loading