Skip to content

Commit

Permalink
adding new Action for Mixpanel (#1802)
Browse files Browse the repository at this point in the history
* adding new Action for Mixpanel

* max increments value change
  • Loading branch information
joe-ayoub-segment authored Jan 16, 2024
1 parent 8ef31b5 commit 3cee076
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for Mixpanel's identifyUser destination action: all fields 1`] = `"data=%7B%22event%22%3A%22%24identify%22%2C%22properties%22%3A%7B%22%24identified_id%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22%24anon_id%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22token%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22segment_source_name%22%3A%221SVsemB5FYy7%23Wu9%22%7D%7D"`;

exports[`Testing snapshot for Mixpanel's identifyUser destination action: required fields 1`] = `"data=%7B%22event%22%3A%22%24identify%22%2C%22properties%22%3A%7B%22%24identified_id%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22%24anon_id%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22token%22%3A%221SVsemB5FYy7%23Wu9%22%2C%22segment_source_name%22%3A%221SVsemB5FYy7%23Wu9%22%7D%7D"`;

exports[`Testing snapshot for Mixpanel's identifyUser destination action: required fields 2`] = `
Headers {
Symbol(map): Object {
"Content-Type": Array [
"application/x-www-form-urlencoded;charset=UTF-8",
],
"user-agent": Array [
"Segment (Actions)",
],
},
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'
import { ApiRegions } from '../../common/utils'

const testDestination = createTestIntegration(Destination)
const MIXPANEL_API_SECRET = 'test-api-key'
const MIXPANEL_PROJECT_TOKEN = 'test-proj-token'
const timestamp = '2021-08-17T15:21:15.449Z'

describe('Mixpanel.incrementProperties', () => {
const defaultProperties = { term: 'foo', increment: { searches: 1 } }
it('should use EU server URL', async () => {
const event = createTestEvent({ timestamp, event: 'search', properties: defaultProperties })

nock('https://api-eu.mixpanel.com').post('/engage').reply(200, {})
nock('https://api-eu.mixpanel.com').post('/track').reply(200, {})

const responses = await testDestination.testAction('incrementProperties', {
event,
useDefaultMappings: true,
settings: {
projectToken: MIXPANEL_PROJECT_TOKEN,
apiSecret: MIXPANEL_API_SECRET,
apiRegion: ApiRegions.EU
}
})

expect(responses[0].status).toBe(200)
expect(responses[0].data).toMatchObject({})
expect(responses[0].options.body).toMatchObject(
new URLSearchParams({
data: JSON.stringify({
$token: MIXPANEL_PROJECT_TOKEN,
$distinct_id: 'user1234',
$ip: '8.8.8.8',
$add: {
searches: 1
}
})
})
)
})

it('should default to US endpoint if apiRegion setting is undefined', async () => {
const event = createTestEvent({ timestamp, event: 'search', properties: defaultProperties })

nock('https://api.mixpanel.com').post('/engage').reply(200, {})
nock('https://api.mixpanel.com').post('/track').reply(200, {})

const responses = await testDestination.testAction('incrementProperties', {
event,
useDefaultMappings: true,
settings: {
projectToken: MIXPANEL_PROJECT_TOKEN,
apiSecret: MIXPANEL_API_SECRET
}
})

expect(responses[0].status).toBe(200)
expect(responses[0].data).toMatchObject({})
expect(responses[0].options.body).toMatchObject(
new URLSearchParams({
data: JSON.stringify({
$token: MIXPANEL_PROJECT_TOKEN,
$distinct_id: 'user1234',
$ip: '8.8.8.8',
$add: {
searches: 1
}
})
})
)
})

it('should use anonymous_id as distinct_id if user_id is missing', async () => {
const event = createTestEvent({ userId: null, event: 'search', properties: defaultProperties })

nock('https://api.mixpanel.com').post('/track').reply(200, {})
nock('https://api.mixpanel.com').post('/engage').reply(200, {})

const responses = await testDestination.testAction('incrementProperties', {
event,
useDefaultMappings: true,
settings: {
projectToken: MIXPANEL_PROJECT_TOKEN,
apiSecret: MIXPANEL_API_SECRET
}
})

expect(responses[0].status).toBe(200)
expect(responses[0].data).toMatchObject({})
expect(responses[0].options.body).toMatchObject(
new URLSearchParams({
data: JSON.stringify({
$token: MIXPANEL_PROJECT_TOKEN,
$distinct_id: event.anonymousId,
$ip: '8.8.8.8',
$add: {
searches: 1
}
})
})
)
})

it('should $add values to increment numerical properties', async () => {
const event = createTestEvent({
timestamp,
event: 'search',
properties: {
abc: '123',
increment: {
positive: 2,
negative: -2
}
}
})

nock('https://api.mixpanel.com').post('/track').reply(200, {})
nock('https://api.mixpanel.com').post('/engage').reply(200, {})

const responses = await testDestination.testAction('incrementProperties', {
event,
useDefaultMappings: true,
settings: {
projectToken: MIXPANEL_PROJECT_TOKEN,
apiSecret: MIXPANEL_API_SECRET
}
})

expect(responses[0].status).toBe(200)
expect(responses[0].data).toMatchObject({})
expect(responses[0].options.body).toMatchObject(
new URLSearchParams({
data: JSON.stringify({
$token: MIXPANEL_PROJECT_TOKEN,
$distinct_id: 'user1234',
$ip: '8.8.8.8',
$add: {
positive: 2,
negative: -2
}
})
})
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import { generateTestData } from '../../../../lib/test-data'
import destination from '../../index'
import nock from 'nock'

const testDestination = createTestIntegration(destination)
const actionSlug = 'identifyUser'
const destinationSlug = 'Mixpanel'
const seedName = `${destinationSlug}#${actionSlug}`

describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => {
it('required fields', async () => {
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, true)

nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({
properties: eventData
})

const responses = await testDestination.testAction(actionSlug, {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
})

const request = responses[0].request
const rawBody = await request.text()

try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}

expect(request.headers).toMatchSnapshot()
})

it('all fields', async () => {
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, false)

nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({
properties: eventData
})

const responses = await testDestination.testAction(actionSlug, {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
})

const request = responses[0].request
const rawBody = await request.text()

try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ActionDefinition, IntegrationError, PayloadValidationError } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import { MixpanelEngageProperties } from '../mixpanel-types'
import { getApiServerUrl } from '../common/utils'
import type { Payload } from './generated-types'

const action: ActionDefinition<Settings, Payload> = {
title: 'Increment Properties',
description:
'Increment the value of a user profile property. [Learn More](https://developer.mixpanel.com/reference/profile-numerical-add).',
defaultSubscription: 'type = "track"',
fields: {
ip: {
label: 'IP Address',
type: 'string',
description: "The IP address of the user. This is only used for geolocation and won't be stored.",
default: {
'@path': '$.context.ip'
}
},
user_id: {
label: 'User ID',
type: 'string',
allowNull: true,
description: 'The unique user identifier set by you',
default: {
'@path': '$.userId'
}
},
anonymous_id: {
label: 'Anonymous ID',
type: 'string',
allowNull: true,
description: 'The generated anonymous ID for the user',
default: {
'@path': '$.anonymousId'
}
},
increment: {
label: 'Increment Numerical Properties',
type: 'object',
description:
'Object of properties and the values to increment or decrement. For example: `{"purchases": 1, "items": 6}}.',
multiple: false,
required: true,
defaultObjectUI: 'keyvalue',
default: {
'@path': '$.properties.increment'
}
}
},

perform: async (request, { payload, settings }) => {
if (!settings.projectToken) {
throw new IntegrationError('Missing project token', 'Missing required field', 400)
}

const apiServerUrl = getApiServerUrl(settings.apiRegion)

const responses = []

if (payload.increment && Object.keys(payload.increment).length > 0) {
const keys = Object.keys(payload.increment)
if (keys.length > 20) {
throw new PayloadValidationError('Exceeded maximum of 20 properties for increment call')
}
const data: MixpanelEngageProperties = {
$token: settings.projectToken,
$distinct_id: payload.user_id ?? payload.anonymous_id,
$ip: payload.ip
}
data.$add = {}

for (const key of keys) {
const value = payload.increment[key]
if (typeof value === 'string' || typeof value === 'number') {
if (isNaN(+value)) {
throw new IntegrationError(`The key "${key}" was not numeric`, 'Non numeric increment value', 400)
}
data.$add[key] = +value
}
}

const response = request(`${apiServerUrl}/engage`, {
method: 'post',
body: new URLSearchParams({ data: JSON.stringify(data) })
})

responses.push(response)
}

return Promise.all(responses)
}
}

export default action
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Settings } from './generated-types'

import identifyUser from './identifyUser'
import groupIdentifyUser from './groupIdentifyUser'
import incrementProperties from './incrementProperties'

import alias from './alias'
import { ApiRegions, StrictMode } from './common/utils'
Expand Down Expand Up @@ -126,7 +127,8 @@ const destination: DestinationDefinition<Settings> = {
identifyUser,
groupIdentifyUser,
alias,
trackPurchase
trackPurchase,
incrementProperties
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,7 @@ export type MixpanelEngageProperties = {
$distinct_id?: string | null
$ip?: string
$set?: MixpanelEngageSet
$add?: MixpanelIncrementPropertiesObject
}

export type MixpanelIncrementPropertiesObject = { [key: string]: number }

0 comments on commit 3cee076

Please sign in to comment.