Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Definition from '../index'

const testDestination = createTestIntegration(Definition)

describe('Lark', () => {
describe('testAuthentication', () => {
it('should validate authentication inputs', async () => {
nock('https://api.uselark.ai').get('/subjects').reply(200, { data: [] })

const authData = { apiKey: 'test-api-key' }

await expect(testDestination.testAuthentication(authData)).resolves.not.toThrowError()
})

it('should fail with invalid API key', async () => {
nock('https://api.uselark.ai').get('/subjects').reply(401, { error: 'Unauthorized' })

const authData = { apiKey: 'invalid-api-key' }

await expect(testDestination.testAuthentication(authData)).rejects.toThrowError()
})
})

describe('createUsageEvent', () => {
it('should send a usage event to Lark', async () => {
nock('https://api.uselark.ai').post('/usage-events').reply(200, {})

const event = createTestEvent({
type: 'track',
event: 'Job Completed',
userId: 'user-123',
messageId: 'msg-123',
timestamp: '2024-01-15T10:30:00.000Z',
properties: {
compute_hours: 100.5,
instance_type: 't2.micro',
region: 'us-east-1'
}
})

const responses = await testDestination.testAction('createUsageEvent', {
event,
settings: { apiKey: 'test-api-key' },
mapping: {
event_name: { '@path': '$.event' },
subject_id: { '@path': '$.userId' },
idempotency_key: { '@path': '$.messageId' },
timestamp: { '@path': '$.timestamp' },
data: { '@path': '$.properties' }
}
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)

const requestBody = JSON.parse(responses[0].options.body as string)
expect(requestBody.event_name).toBe('Job Completed')
expect(requestBody.subject_id).toBe('user-123')
expect(requestBody.idempotency_key).toBe('msg-123')
expect(requestBody.data.compute_hours).toBe(100.5)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 destinationSlug = 'actions-lark'

describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
for (const actionSlug in destination.actions) {
it(`${actionSlug} action - required fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
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(`${actionSlug} action - all fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
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,77 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'

const action: ActionDefinition<Settings, Payload> = {
title: 'Create Usage Event',
description: 'Send a usage event to Lark for billing and metering purposes.',
defaultSubscription: 'type = "track"',
fields: {
event_name: {
label: 'Event Name',
description: 'The name of the event. This is used by pricing metrics to aggregate usage events.',
type: 'string',
required: true,
default: {
'@path': '$.event'
}
},
subject_id: {
label: 'Subject ID',
description: 'The ID or external ID of the subject that the usage event is for.',
type: 'string',
required: true,
default: {
'@path': '$.userId'
}
},
idempotency_key: {
label: 'Idempotency Key',
description:
'The idempotency key for the usage event. This ensures that the same event is not processed multiple times.',
type: 'string',
required: true,
default: {
'@path': '$.messageId'
}
},
timestamp: {
label: 'Timestamp',
description:
'The timestamp of the usage event (ISO 8601 format). If not provided, the current timestamp will be used.',
type: 'datetime',
required: false,
default: {
'@path': '$.timestamp'
}
},
data: {
label: 'Data',
description:
'The data of the usage event. This should contain any data that is needed to aggregate the usage event.',
type: 'object',
required: false,
default: {
'@path': '$.properties'
}
}
},
perform: (request, { settings, payload }) => {
return request('https://api.uselark.ai/usage-events', {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-API-Key': settings.apiKey
},
json: {
event_name: payload.event_name,
subject_id: payload.subject_id,
idempotency_key: payload.idempotency_key,
timestamp: payload.timestamp,
data: payload.data
}
})
}
}

export default action

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

59 changes: 59 additions & 0 deletions packages/destination-actions/src/destinations/lark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { DestinationDefinition } from '@segment/actions-core'
import { defaultValues } from '@segment/actions-core'
import type { Settings } from './generated-types'

import createUsageEvent from './createUsageEvent'

const destination: DestinationDefinition<Settings> = {
name: 'Lark',
slug: 'actions-lark',
mode: 'cloud',
description: 'Send usage events to Lark for billing and metering purposes.',

authentication: {
scheme: 'custom',
fields: {
apiKey: {
label: 'API Key',
description: 'Your Lark API key. You can find this in your Lark dashboard.',
type: 'password',
required: true
}
},
testAuthentication: async (request, { settings }) => {
// Test the API key by making a request to list subjects (a lightweight endpoint)
// If the API key is invalid, this will return an error
return request('https://api.uselark.ai/subjects', {
method: 'get',
headers: {
'X-API-Key': settings.apiKey
}
})
}
},

extendRequest({ settings }) {
return {
headers: {
'X-API-Key': settings.apiKey,
'Content-Type': 'application/json'
}
}
},

actions: {
createUsageEvent
},

presets: [
{
name: 'Send Usage Event to Lark',
subscribe: 'type = "track"',
partnerAction: 'createUsageEvent',
mapping: defaultValues(createUsageEvent.fields),
type: 'automatic'
}
]
}

export default destination
Loading