Skip to content

Commit 3b7f30c

Browse files
authored
feat: send $device_id on feature flag requests (#2708)
* send device_id on feature flag requests * add changeset * include in mock * use isNull and isUndefined instead of direct checks * update functional tests
1 parent 59f7145 commit 3b7f30c

File tree

5 files changed

+152
-1
lines changed

5 files changed

+152
-1
lines changed

.changeset/dirty-news-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'posthog-js': patch
3+
---
4+
5+
Include $device_id when fetching feature flags

packages/browser/functional_tests/feature-flags.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('FunctionalTests / Feature Flags', () => {
2727
expect(getRequests(token)['/flags/']).toEqual([
2828
// This is the initial call to the flags endpoint on PostHog init.
2929
{
30+
$device_id: anonymousId,
3031
distinct_id: anonymousId,
3132
person_properties: {},
3233
groups: {},
@@ -53,6 +54,7 @@ describe('FunctionalTests / Feature Flags', () => {
5354
// `identify()`.
5455
{
5556
$anon_distinct_id: anonymousId,
57+
$device_id: anonymousId,
5658
distinct_id: 'test-id',
5759
person_properties: {
5860
$initial__kx: null,
@@ -102,6 +104,7 @@ describe('FunctionalTests / Feature Flags', () => {
102104
expect(getRequests(token)['/flags/']).toEqual([
103105
// This is the initial call to the flags endpoint on PostHog init.
104106
{
107+
$device_id: anonymousId,
105108
distinct_id: anonymousId,
106109
person_properties: {},
107110
groups: {},
@@ -125,6 +128,7 @@ describe('FunctionalTests / Feature Flags', () => {
125128
// `identify()`.
126129
{
127130
$anon_distinct_id: anonymousId,
131+
$device_id: anonymousId,
128132
distinct_id: 'test-id',
129133
groups: {},
130134
person_properties: {
@@ -172,6 +176,7 @@ describe('FunctionalTests / Feature Flags', () => {
172176
await waitFor(() => {
173177
expect(getRequests(token)['/flags/']).toEqual([
174178
{
179+
$device_id: anonymousId,
175180
distinct_id: 'test-id',
176181
groups: {},
177182
person_properties: {
@@ -221,6 +226,7 @@ describe('FunctionalTests / Feature Flags', () => {
221226
expect(getRequests(token)['/flags/']).toEqual([
222227
// This is the initial call to the flags endpoint on PostHog init.
223228
{
229+
$device_id: anonymousId,
224230
distinct_id: anonymousId,
225231
person_properties: {},
226232
groups: {},
@@ -248,6 +254,7 @@ describe('FunctionalTests / Feature Flags', () => {
248254
expect(getRequests(token)['/flags/']).toEqual([
249255
{
250256
$anon_distinct_id: anonymousId,
257+
$device_id: anonymousId,
251258
distinct_id: 'test-id',
252259
groups: {},
253260
person_properties: {
@@ -304,6 +311,7 @@ describe('FunctionalTests / Feature Flags', () => {
304311
// This is the initial call to the flags endpoint on PostHog init, with all info added from `loaded`.
305312
{
306313
$anon_distinct_id: 'anon-id',
314+
$device_id: 'anon-id',
307315
distinct_id: 'test-id',
308316
groups: { playlist: 'id:77' },
309317
person_properties: {

packages/browser/playwright/mocked/flags.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ test.describe('flags', () => {
6868
expect(flagsPayload).toEqual({
6969
token: 'test token',
7070
distinct_id: 'new-id',
71+
$device_id: flagsPayload.$device_id,
7172
person_properties: {
7273
$initial__kx: null,
7374
$initial_current_url: 'http://localhost:2345/playground/cypress/index.html',

packages/browser/src/__tests__/featureflags.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,136 @@ describe('featureflags', () => {
11691169
})
11701170
})
11711171

1172+
describe('device_id in flags requests', () => {
1173+
beforeEach(() => {
1174+
// Clear persistence before each test in this suite
1175+
instance.persistence.unregister('$device_id')
1176+
instance.persistence.unregister('$stored_person_properties')
1177+
instance.persistence.unregister('$stored_group_properties')
1178+
1179+
instance._send_request = jest.fn().mockImplementation(({ callback }) =>
1180+
callback({
1181+
statusCode: 200,
1182+
json: {
1183+
featureFlags: {
1184+
first: 'variant-1',
1185+
second: true,
1186+
},
1187+
},
1188+
})
1189+
)
1190+
})
1191+
1192+
afterEach(() => {
1193+
// Clean up after each test
1194+
instance.persistence.unregister('$device_id')
1195+
instance.persistence.unregister('$stored_person_properties')
1196+
instance.persistence.unregister('$stored_group_properties')
1197+
})
1198+
1199+
it('should include device_id in flags request when available', () => {
1200+
instance.persistence.register({
1201+
$device_id: 'test-device-uuid-123',
1202+
})
1203+
1204+
featureFlags.reloadFeatureFlags()
1205+
jest.runAllTimers()
1206+
1207+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1208+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1209+
token: 'random fake token',
1210+
distinct_id: 'blah id',
1211+
$device_id: 'test-device-uuid-123',
1212+
person_properties: {},
1213+
})
1214+
})
1215+
1216+
it('should omit device_id when it is null (cookieless mode)', () => {
1217+
instance.persistence.register({
1218+
$device_id: null,
1219+
})
1220+
1221+
featureFlags.reloadFeatureFlags()
1222+
jest.runAllTimers()
1223+
1224+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1225+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1226+
token: 'random fake token',
1227+
distinct_id: 'blah id',
1228+
person_properties: {},
1229+
})
1230+
expect(instance._send_request.mock.calls[0][0].data).not.toHaveProperty('$device_id')
1231+
})
1232+
1233+
it('should omit device_id when it is undefined', () => {
1234+
// Don't register device_id at all
1235+
featureFlags.reloadFeatureFlags()
1236+
jest.runAllTimers()
1237+
1238+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1239+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1240+
token: 'random fake token',
1241+
distinct_id: 'blah id',
1242+
person_properties: {},
1243+
})
1244+
expect(instance._send_request.mock.calls[0][0].data).not.toHaveProperty('$device_id')
1245+
})
1246+
1247+
it('should include device_id along with $anon_distinct_id on identify', () => {
1248+
instance.persistence.register({
1249+
$device_id: 'device-uuid-456',
1250+
})
1251+
1252+
featureFlags.setAnonymousDistinctId('anon_id_789')
1253+
featureFlags.reloadFeatureFlags()
1254+
jest.runAllTimers()
1255+
1256+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1257+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1258+
token: 'random fake token',
1259+
distinct_id: 'blah id',
1260+
$device_id: 'device-uuid-456',
1261+
$anon_distinct_id: 'anon_id_789',
1262+
person_properties: {},
1263+
})
1264+
})
1265+
1266+
it('should include device_id with person_properties', () => {
1267+
instance.persistence.register({
1268+
$device_id: 'device-uuid-999',
1269+
})
1270+
1271+
featureFlags.setPersonPropertiesForFlags({ plan: 'pro', beta_tester: true })
1272+
jest.runAllTimers()
1273+
1274+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1275+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1276+
token: 'random fake token',
1277+
distinct_id: 'blah id',
1278+
$device_id: 'device-uuid-999',
1279+
person_properties: { plan: 'pro', beta_tester: true },
1280+
})
1281+
})
1282+
1283+
it('should include device_id with group_properties', () => {
1284+
instance.persistence.register({
1285+
$device_id: 'device-uuid-888',
1286+
})
1287+
1288+
featureFlags.setGroupPropertiesForFlags({ company: { name: 'Acme', seats: 50 } })
1289+
jest.runAllTimers()
1290+
1291+
expect(instance._send_request).toHaveBeenCalledTimes(1)
1292+
expect(instance._send_request.mock.calls[0][0].data).toEqual({
1293+
token: 'random fake token',
1294+
distinct_id: 'blah id',
1295+
$device_id: 'device-uuid-888',
1296+
person_properties: {},
1297+
group_properties: { company: { name: 'Acme', seats: 50 } },
1298+
})
1299+
})
1300+
})
1301+
11721302
describe('reloadFeatureFlags', () => {
11731303
beforeEach(() => {
11741304
instance._send_request = jest.fn().mockImplementation(({ callback }) =>

packages/browser/src/posthog-featureflags.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
FLAG_CALL_REPORTED,
2525
} from './constants'
2626

27-
import { isUndefined, isArray } from '@posthog/core'
27+
import { isUndefined, isArray, isNull } from '@posthog/core'
2828
import { createLogger } from './utils/logger'
2929
import { getTimezone } from './utils/event-utils'
3030

@@ -398,6 +398,8 @@ export class PostHogFeatureFlags {
398398
return
399399
}
400400
const token = this._instance.config.token
401+
const deviceId = this._instance.get_property('$device_id')
402+
401403
const data: Record<string, any> = {
402404
token: token,
403405
distinct_id: this._instance.get_distinct_id(),
@@ -410,6 +412,11 @@ export class PostHogFeatureFlags {
410412
group_properties: this._instance.get_property(STORED_GROUP_PROPERTIES_KEY),
411413
}
412414

415+
// Add device_id if available (handle cookieless mode where it's null)
416+
if (!isNull(deviceId) && !isUndefined(deviceId)) {
417+
data.$device_id = deviceId
418+
}
419+
413420
if (options?.disableFlags || this._instance.config.advanced_disable_feature_flags) {
414421
data.disable_flags = true
415422
}

0 commit comments

Comments
 (0)