Skip to content

Commit

Permalink
Launchpad Segment Integration (#1010)
Browse files Browse the repository at this point in the history
* Launchpad Segment Integration

We are now adding the ability to send events from segment into Launchpad.pm - the mission control for Product teams at scale.

We are adding the following:
* trackEvent
* groupIdentifyUser
* identifyUser

We have added unit tests and have tested this manually.

It requires the following:
* apiRegion - EU by default. We have added the US as means of future extensibility.
* apiSecret - given by Launchpad.pm while onboarding.
* sourceName - to be added.

* updating snapshots

* Changes implemented following call with Joe

Track()

[x] Explicitly indicate which fields are required or optional
[x] Look for traits and if not context.traits
[x] Need description for the Action
[x] Make Timestamp field required
[x] Make messageId required. It will always be there.
[x] getEventProperties: This line looks incorrect: id: payload.event,
[x] source mapping looks incorrect: source: integration?.name == 'Iterable' ? 'Iterable' : 'segment',

Identify()

[x] Description should be updated.
[x] identify() calls are often fired when a trait is collected, or when a userId is collected.
[x] Maybe use wording: “Creates or updates a user profile, and adds or updates trait values on the user profile…”
[x] Refactor the perform() function

group()

[x] Group Key - Amend description to better explain that group key is a way to connect multiple organizations together
[x] groupId field - Should be updated
[x] Consider adding anonymousId as a field
[x] Handle when there are no traits.

* changes

* tests passing, removed the check on user_id, anonymous_id since none are required

* fixing the types as well

* fixing test

---------

Co-authored-by: joe-ayoub-segment <[email protected]>
  • Loading branch information
SSabev and joe-ayoub-segment authored Feb 8, 2023
1 parent 15b000c commit b81dd49
Show file tree
Hide file tree
Showing 20 changed files with 1,119 additions and 0 deletions.

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,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for Launchpad's groupIdentifyUser destination action: all fields 1`] = `
Object {
"$set": Object {
"group_id": "$WBBWnR",
"group_testType": "$WBBWnR",
},
"anonymoud_id": "$WBBWnR",
"api_key": "$WBBWnR",
"distinct_id": "$WBBWnR",
"event": "$identify",
"type": "screen",
"user_id": "$WBBWnR",
}
`;

exports[`Testing snapshot for Launchpad's groupIdentifyUser destination action: required fields 1`] = `
Object {
"$set": Object {
"group_id": "$WBBWnR",
},
"api_key": "$WBBWnR",
"event": "$identify",
"type": "screen",
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import nock from 'nock'
import { createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'
import { SegmentEvent } from '@segment/actions-core'

const launchpadAPISecret = 'lp-api-key'
const timestamp = '2023-01-28T15:21:15.449Z'

const testDestination = createTestIntegration(Destination)

const expectedTraits = {
group_name: 'Launchpad',
group_industry: 'Technology',
group_employees: 3,
group_plan: '1',
'group_ARR(m)': 1503
}

const testGroupIdentify: SegmentEvent = {
messageId: 'test-message-t73406chv4',
timestamp: timestamp,
type: 'group',
groupId: '12381923812',
userId: '[email protected]',
traits: {
name: 'Launchpad',
industry: 'Technology',
employees: 3,
plan: '1',
'ARR(m)': 1503
}
}

describe('Launchpad.groupIdentifyUser', () => {
it('should convert the type and event name', async () => {
nock('https://data.launchpad.pm').post('/capture').reply(200, {})

const responses = await testDestination.testAction('groupIdentifyUser', {
event: testGroupIdentify,
useDefaultMappings: true,
settings: {
apiSecret: launchpadAPISecret,
sourceName: 'example segment source name'
}
})
expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(responses[0].data).toMatchObject({})
expect(responses[0].options.json).toMatchObject({
event: '$identify',
type: 'screen',
$set: expect.objectContaining(expectedTraits)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 = 'groupIdentifyUser'
const destinationSlug = 'Launchpad'
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
})
event.userId = 'user1234'

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,89 @@
import { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import { getApiServerUrl } from '../utils'
import type { Payload } from './generated-types'

const groupIdentifyUser: ActionDefinition<Settings, Payload> = {
title: 'Group Identify User',
description:
'Updates or adds properties to a group profile. The profile is created if it does not exist. [Learn more about Group Analytics.](https://help.Launchpad.pm)',
defaultSubscription: 'type = "group"',
fields: {
groupKey: {
label: 'Group Key',
type: 'string',
required: false,
description:
'The group key you specified in Launchpad under the company corresponding to the group. If this is not specified, it will be defaulted to "$group_id". This is helpful when you have a group of companies that should be joined together as in when you have a multinational.'
},
groupId: {
label: 'Group ID',
type: 'string',
description:
'The unique identifier of the group. If there is a trait that matches the group key, it will override this value.',
required: true,
default: {
'@path': '$.groupId'
}
},
traits: {
label: 'Group Properties',
type: 'object',
description: 'The properties to set on the group profile.',
required: false,
default: {
'@path': '$.traits'
}
},
userId: {
label: 'User ID',
type: 'string',
description:
'A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty',
default: {
'@path': '$.userId'
}
},
anonymousId: {
label: 'Anonymous ID',
type: 'string',
description:
'A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty',
default: {
'@path': '$.anonymousId'
}
}
},

perform: async (request, { payload, settings }) => {
const groupId = payload.groupId
const apiServerUrl = getApiServerUrl(settings.apiRegion)
let transformed_traits

if (payload.traits) {
transformed_traits = {
...Object.fromEntries(Object.entries(payload.traits).map(([k, v]) => [`group_${k}`, v]))
}
}
const groupIdentifyEvent = {
event: '$identify',
type: 'screen',
$set: {
group_id: groupId,
...transformed_traits
},
distinct_id: payload.userId ? payload.userId : payload.anonymousId,
user_id: payload.userId,
anonymoud_id: payload.anonymousId,
api_key: settings.apiSecret
}
const groupIdentifyResponse = await request(`${apiServerUrl}capture`, {
method: 'post',
json: groupIdentifyEvent
})

return groupIdentifyResponse
}
}

export default groupIdentifyUser
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for Launchpad's identifyUser destination action: all fields 1`] = `
Object {
"$ip": "5SpoCjYek1jtNwUC",
"$set": Object {
"testType": "5SpoCjYek1jtNwUC",
},
"anonymous_id": "5SpoCjYek1jtNwUC",
"api_key": "5SpoCjYek1jtNwUC",
"distinct_id": "5SpoCjYek1jtNwUC",
"event": "$identify",
"type": "screen",
"user_id": "5SpoCjYek1jtNwUC",
}
`;

exports[`Testing snapshot for Launchpad's identifyUser destination action: required fields 1`] = `
Object {
"$set": Object {
"testType": "5SpoCjYek1jtNwUC",
},
"api_key": "5SpoCjYek1jtNwUC",
"distinct_id": "user1234",
"event": "$identify",
"type": "screen",
"user_id": "user1234",
}
`;
Loading

0 comments on commit b81dd49

Please sign in to comment.