mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
fix(msg): no empty objects linkedin ads (#38905)
Co-authored-by: Haven <haven@posthog.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { DateTime, Settings } from 'luxon'
|
||||
|
||||
import { TemplateTester } from '../../test/test-helpers'
|
||||
import { template } from './linkedin.template'
|
||||
|
||||
jest.setTimeout(60 * 1000)
|
||||
|
||||
const buildInputs = (inputs: Record<string, any> = {}): Record<string, any> => {
|
||||
return {
|
||||
oauth: { access_token: 'oauth-1234' },
|
||||
accountId: 'account-12345',
|
||||
conversionRuleId: 'conversion-rule-12345',
|
||||
conversionDateTime: 1737464596570,
|
||||
conversionValue: '100',
|
||||
currencyCode: 'USD',
|
||||
eventId: 'event-12345',
|
||||
userIds: {
|
||||
SHA256_EMAIL: '3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98',
|
||||
LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID: 'abc',
|
||||
},
|
||||
userInfo: { lastName: 'AI', firstName: 'Max', companyName: 'PostHog', countryCode: 'US' },
|
||||
...inputs,
|
||||
}
|
||||
}
|
||||
|
||||
describe('linkedin template', () => {
|
||||
const tester = new TemplateTester(template)
|
||||
beforeEach(async () => {
|
||||
Settings.defaultZone = 'UTC'
|
||||
await tester.beforeEach()
|
||||
const fixedTime = DateTime.fromISO('2025-01-01T00:00:00Z').toJSDate()
|
||||
jest.spyOn(Date, 'now').mockReturnValue(fixedTime.getTime())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Settings.defaultZone = 'system'
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('works with all properties', async () => {
|
||||
const response = await tester.invoke(buildInputs())
|
||||
|
||||
expect(response.error).toBeUndefined()
|
||||
expect(response.finished).toEqual(false)
|
||||
expect(response.invocation.queueParameters).toMatchInlineSnapshot(`
|
||||
{
|
||||
"body": "{"conversion":"urn:lla:llaPartnerConversion:conversion-rule-12345","conversionHappenedAt":1737464596570,"user":{"userIds":[{"idType":"SHA256_EMAIL","idValue":"3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98"},{"idType":"LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID","idValue":"abc"}],"userInfo":{"lastName":"AI","firstName":"Max","companyName":"PostHog","countryCode":"US"}},"eventId":"event-12345","conversionValue":{"currencyCode":"USD","amount":"100"}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer oauth-1234",
|
||||
"Content-Type": "application/json",
|
||||
"LinkedIn-Version": "202508",
|
||||
},
|
||||
"method": "POST",
|
||||
"type": "fetch",
|
||||
"url": "https://api.linkedin.com/rest/conversionEvents",
|
||||
}
|
||||
`)
|
||||
|
||||
const fetchResponse = await tester.invokeFetchResponse(response.invocation, {
|
||||
status: 200,
|
||||
body: { status: 'OK' },
|
||||
})
|
||||
|
||||
expect(fetchResponse.finished).toBe(true)
|
||||
expect(fetchResponse.error).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not contain empty objects', async () => {
|
||||
const response = await tester.invoke(
|
||||
buildInputs({
|
||||
conversionValue: null,
|
||||
currencyCode: null,
|
||||
userInfo: {},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.invocation.queueParameters).toMatchInlineSnapshot(`
|
||||
{
|
||||
"body": "{"conversion":"urn:lla:llaPartnerConversion:conversion-rule-12345","conversionHappenedAt":1737464596570,"user":{"userIds":[{"idType":"SHA256_EMAIL","idValue":"3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98"},{"idType":"LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID","idValue":"abc"}]},"eventId":"event-12345"}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer oauth-1234",
|
||||
"Content-Type": "application/json",
|
||||
"LinkedIn-Version": "202508",
|
||||
},
|
||||
"method": "POST",
|
||||
"type": "fetch",
|
||||
"url": "https://api.linkedin.com/rest/conversionEvents",
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,162 @@
|
||||
import { HogFunctionTemplate } from '~/cdp/types'
|
||||
|
||||
export const template: HogFunctionTemplate = {
|
||||
free: false,
|
||||
status: 'alpha',
|
||||
type: 'destination',
|
||||
id: 'template-linkedin-ads',
|
||||
name: 'LinkedIn Ads Conversions',
|
||||
description: 'Send conversion events to LinkedIn Ads',
|
||||
icon_url: '/static/services/linkedin.png',
|
||||
category: ['Advertisement'],
|
||||
code_language: 'hog',
|
||||
code: `
|
||||
let body := {
|
||||
'conversion': f'urn:lla:llaPartnerConversion:{inputs.conversionRuleId}',
|
||||
'conversionHappenedAt': inputs.conversionDateTime,
|
||||
'user': {
|
||||
'userIds': []
|
||||
},
|
||||
'eventId' : inputs.eventId
|
||||
}
|
||||
|
||||
if (not empty(inputs.conversionValue) or not empty(inputs.currencyCode)) {
|
||||
body.conversionValue := {}
|
||||
}
|
||||
if (not empty(inputs.currencyCode)) {
|
||||
body.conversionValue.currencyCode := inputs.currencyCode
|
||||
}
|
||||
if (not empty(inputs.conversionValue)) {
|
||||
body.conversionValue.amount := inputs.conversionValue
|
||||
}
|
||||
|
||||
let userInfo := {}
|
||||
for (let key, value in inputs.userInfo) {
|
||||
if (not empty(value)) {
|
||||
userInfo[key] := value
|
||||
}
|
||||
}
|
||||
if (length(keys(userInfo)) >= 1) {
|
||||
body.user['userInfo'] := userInfo
|
||||
}
|
||||
|
||||
for (let key, value in inputs.userIds) {
|
||||
if (not empty(value)) {
|
||||
body.user.userIds := arrayPushBack(body.user.userIds, {'idType': key, 'idValue': value})
|
||||
}
|
||||
}
|
||||
|
||||
let res := fetch('https://api.linkedin.com/rest/conversionEvents', {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'Authorization': f'Bearer {inputs.oauth.access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202508'
|
||||
},
|
||||
'body': body
|
||||
})
|
||||
|
||||
if (res.status >= 400) {
|
||||
throw Error(f'Error from api.linkedin.com (status {res.status}): {res.body}')
|
||||
}
|
||||
`,
|
||||
inputs_schema: [
|
||||
{
|
||||
key: 'oauth',
|
||||
type: 'integration',
|
||||
integration: 'linkedin-ads',
|
||||
label: 'LinkedIn Ads account',
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'accountId',
|
||||
type: 'integration_field',
|
||||
integration_key: 'oauth',
|
||||
integration_field: 'linkedin_ads_account_id',
|
||||
label: 'Account ID',
|
||||
description: 'ID of your LinkedIn Ads Account. This should be 9-digits and in XXXXXXXXX format.',
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'conversionRuleId',
|
||||
type: 'integration_field',
|
||||
integration_key: 'oauth',
|
||||
integration_field: 'linkedin_ads_conversion_rule_id',
|
||||
requires_field: 'accountId',
|
||||
label: 'Conversion rule',
|
||||
description: 'The Conversion rule associated with this conversion.',
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'conversionDateTime',
|
||||
type: 'string',
|
||||
label: 'Conversion Date Time',
|
||||
description:
|
||||
'The timestamp at which the conversion occurred in milliseconds. Must be after the click time.',
|
||||
default: '{toUnixTimestampMilli(event.timestamp)}',
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'conversionValue',
|
||||
type: 'string',
|
||||
label: 'Conversion value',
|
||||
description: 'The value of the conversion for the advertiser in decimal string. (e.g. “100.05”).',
|
||||
default: '',
|
||||
secret: false,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'currencyCode',
|
||||
type: 'string',
|
||||
label: 'Currency code',
|
||||
description:
|
||||
'Currency associated with the conversion value. This is the ISO 4217 3-character currency code. For example: USD, EUR.',
|
||||
default: '',
|
||||
secret: false,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'eventId',
|
||||
type: 'string',
|
||||
label: 'Event ID',
|
||||
description: 'ID of the event that triggered the conversion.',
|
||||
default: '{event.uuid}',
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'userIds',
|
||||
type: 'dictionary',
|
||||
label: 'User ids',
|
||||
description:
|
||||
'A map that contains user ids. See this page for options: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-03&tabs=curl#idtype',
|
||||
default: {
|
||||
SHA256_EMAIL: '{sha256Hex(person.properties.email)}',
|
||||
LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID:
|
||||
'{person.properties.li_fat_id ?? person.properties.$initial_li_fat_id}',
|
||||
},
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'userInfo',
|
||||
type: 'dictionary',
|
||||
label: 'User information',
|
||||
description:
|
||||
'A map that contains user information data. See this page for options: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-03&tabs=curl#userinfo',
|
||||
default: {
|
||||
firstName: '{person.properties.first_name}',
|
||||
lastName: '{person.properties.last_name}',
|
||||
title: '{person.properties.title}',
|
||||
companyName: '{person.properties.company}',
|
||||
countryCode: '{person.properties.$geoip_country_code}',
|
||||
},
|
||||
secret: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { template as googleTagManagerTemplate } from './_destinations/google-tag
|
||||
import { template as googleAdsTemplate } from './_destinations/google_ads/google.template'
|
||||
import { template as googleSheetsTemplate } from './_destinations/google_sheets/google_sheets.template'
|
||||
import { template as linearTemplate } from './_destinations/linear/linear.template'
|
||||
import { template as linkedinAdsTemplate } from './_destinations/linkedin_ads/linkedin.template'
|
||||
import { template as nativeWebhookTemplate } from './_destinations/native_webhook/webhook.template'
|
||||
import { template as posthogCaptureTemplate } from './_destinations/posthog_capture/posthog-capture.template'
|
||||
import { template as posthogGroupIdentifyTemplate } from './_destinations/posthog_capture/posthog-group-identify.template'
|
||||
@@ -42,6 +43,7 @@ export const HOG_FUNCTION_TEMPLATES_DESTINATIONS: HogFunctionTemplate[] = [
|
||||
linearTemplate,
|
||||
githubTemplate,
|
||||
googleAdsTemplate,
|
||||
linkedinAdsTemplate,
|
||||
redditAdsTemplate,
|
||||
twilioTemplate,
|
||||
googleSheetsTemplate,
|
||||
|
||||
@@ -48,7 +48,6 @@ from .klaviyo.template_klaviyo import (
|
||||
template_user as klaviyo_user,
|
||||
)
|
||||
from .knock.template_knock import template as knock
|
||||
from .linkedin_ads.template_linkedin_ads import template as linkedin_ads
|
||||
from .loops.template_loops import (
|
||||
TemplateLoopsMigrator,
|
||||
template as loops,
|
||||
@@ -112,7 +111,6 @@ HOG_FUNCTION_TEMPLATES = [
|
||||
klaviyo_event,
|
||||
klaviyo_user,
|
||||
knock,
|
||||
linkedin_ads,
|
||||
loops,
|
||||
loops_send_event,
|
||||
mailchimp,
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
from posthog.cdp.templates.hog_function_template import HogFunctionTemplateDC
|
||||
|
||||
template: HogFunctionTemplateDC = HogFunctionTemplateDC(
|
||||
status="alpha",
|
||||
free=False,
|
||||
type="destination",
|
||||
id="template-linkedin-ads",
|
||||
name="LinkedIn Ads Conversions",
|
||||
description="Send conversion events to LinkedIn Ads",
|
||||
icon_url="/static/services/linkedin.png",
|
||||
category=["Advertisement"],
|
||||
code_language="hog",
|
||||
code="""
|
||||
let body := {
|
||||
'conversion': f'urn:lla:llaPartnerConversion:{inputs.conversionRuleId}',
|
||||
'conversionHappenedAt': inputs.conversionDateTime,
|
||||
'user': {
|
||||
'userIds': [],
|
||||
'userInfo': {}
|
||||
},
|
||||
'eventId' : inputs.eventId
|
||||
}
|
||||
|
||||
if (not empty(inputs.conversionValue) or not empty(inputs.currencyCode)) {
|
||||
body.conversionValue := {}
|
||||
}
|
||||
if (not empty(inputs.currencyCode)) {
|
||||
body.conversionValue.currencyCode := inputs.currencyCode
|
||||
}
|
||||
if (not empty(inputs.conversionValue)) {
|
||||
body.conversionValue.amount := inputs.conversionValue
|
||||
}
|
||||
|
||||
for (let key, value in inputs.userInfo) {
|
||||
if (not empty(value)) {
|
||||
body.user.userInfo[key] := value
|
||||
}
|
||||
}
|
||||
|
||||
for (let key, value in inputs.userIds) {
|
||||
if (not empty(value)) {
|
||||
body.user.userIds := arrayPushBack(body.user.userIds, {'idType': key, 'idValue': value})
|
||||
}
|
||||
}
|
||||
|
||||
let res := fetch('https://api.linkedin.com/rest/conversionEvents', {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'Authorization': f'Bearer {inputs.oauth.access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202508'
|
||||
},
|
||||
'body': body
|
||||
})
|
||||
|
||||
if (res.status >= 400) {
|
||||
throw Error(f'Error from api.linkedin.com (status {res.status}): {res.body}')
|
||||
}
|
||||
""".strip(),
|
||||
inputs_schema=[
|
||||
{
|
||||
"key": "oauth",
|
||||
"type": "integration",
|
||||
"integration": "linkedin-ads",
|
||||
"label": "LinkedIn Ads account",
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "accountId",
|
||||
"type": "integration_field",
|
||||
"integration_key": "oauth",
|
||||
"integration_field": "linkedin_ads_account_id",
|
||||
"label": "Account ID",
|
||||
"description": "ID of your LinkedIn Ads Account. This should be 9-digits and in XXXXXXXXX format.",
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "conversionRuleId",
|
||||
"type": "integration_field",
|
||||
"integration_key": "oauth",
|
||||
"integration_field": "linkedin_ads_conversion_rule_id",
|
||||
"requires_field": "accountId",
|
||||
"label": "Conversion rule",
|
||||
"description": "The Conversion rule associated with this conversion.",
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "conversionDateTime",
|
||||
"type": "string",
|
||||
"label": "Conversion Date Time",
|
||||
"description": "The timestamp at which the conversion occurred in milliseconds. Must be after the click time.",
|
||||
"default": "{toUnixTimestampMilli(event.timestamp)}",
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "conversionValue",
|
||||
"type": "string",
|
||||
"label": "Conversion value",
|
||||
"description": "The value of the conversion for the advertiser in decimal string. (e.g. “100.05”).",
|
||||
"default": "",
|
||||
"secret": False,
|
||||
"required": False,
|
||||
},
|
||||
{
|
||||
"key": "currencyCode",
|
||||
"type": "string",
|
||||
"label": "Currency code",
|
||||
"description": "Currency associated with the conversion value. This is the ISO 4217 3-character currency code. For example: USD, EUR.",
|
||||
"default": "",
|
||||
"secret": False,
|
||||
"required": False,
|
||||
},
|
||||
{
|
||||
"key": "eventId",
|
||||
"type": "string",
|
||||
"label": "Event ID",
|
||||
"description": "ID of the event that triggered the conversion.",
|
||||
"default": "{event.uuid}",
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "userIds",
|
||||
"type": "dictionary",
|
||||
"label": "User ids",
|
||||
"description": "A map that contains user ids. See this page for options: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-03&tabs=curl#idtype",
|
||||
"default": {
|
||||
"SHA256_EMAIL": "{sha256Hex(person.properties.email)}",
|
||||
"LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID": "{person.properties.li_fat_id ?? person.properties.$initial_li_fat_id}",
|
||||
},
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "userInfo",
|
||||
"type": "dictionary",
|
||||
"label": "User information",
|
||||
"description": "A map that contains user information data. See this page for options: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-03&tabs=curl#userinfo",
|
||||
"default": {
|
||||
"firstName": "{person.properties.first_name}",
|
||||
"lastName": "{person.properties.last_name}",
|
||||
"title": "{person.properties.title}",
|
||||
"companyName": "{person.properties.company}",
|
||||
"countryCode": "{person.properties.$geoip_country_code}",
|
||||
},
|
||||
"secret": False,
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
filters={
|
||||
"events": [],
|
||||
"actions": [],
|
||||
"filter_test_accounts": True,
|
||||
},
|
||||
)
|
||||
@@ -1,101 +0,0 @@
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from posthog.cdp.templates.helpers import BaseHogFunctionTemplateTest
|
||||
from posthog.cdp.templates.linkedin_ads.template_linkedin_ads import template as template_linkedin_ads
|
||||
|
||||
|
||||
class TestTemplateLinkedInAds(BaseHogFunctionTemplateTest):
|
||||
template = template_linkedin_ads
|
||||
|
||||
def _inputs(self, **kwargs):
|
||||
inputs = {
|
||||
"oauth": {
|
||||
"access_token": "oauth-1234",
|
||||
},
|
||||
"accountId": "account-12345",
|
||||
"conversionRuleId": "conversion-rule-12345",
|
||||
"conversionDateTime": 1737464596570,
|
||||
"conversionValue": "100",
|
||||
"currencyCode": "USD",
|
||||
"eventId": "event-12345",
|
||||
"userIds": {
|
||||
"SHA256_EMAIL": "3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98",
|
||||
"LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID": "abc",
|
||||
},
|
||||
"userInfo": {"lastName": "AI", "firstName": "Max", "companyName": "PostHog", "countryCode": "US"},
|
||||
}
|
||||
inputs.update(kwargs)
|
||||
return inputs
|
||||
|
||||
def test_function_works(self):
|
||||
self.run_function(self._inputs())
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://api.linkedin.com/rest/conversionEvents",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Bearer oauth-1234",
|
||||
"Content-Type": "application/json",
|
||||
"LinkedIn-Version": "202508",
|
||||
},
|
||||
"body": {
|
||||
"conversion": "urn:lla:llaPartnerConversion:conversion-rule-12345",
|
||||
"conversionHappenedAt": 1737464596570,
|
||||
"user": {
|
||||
"userIds": [
|
||||
{
|
||||
"idType": "SHA256_EMAIL",
|
||||
"idValue": "3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98",
|
||||
},
|
||||
{"idType": "LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID", "idValue": "abc"},
|
||||
],
|
||||
"userInfo": {
|
||||
"lastName": "AI",
|
||||
"firstName": "Max",
|
||||
"companyName": "PostHog",
|
||||
"countryCode": "US",
|
||||
},
|
||||
},
|
||||
"eventId": "event-12345",
|
||||
"conversionValue": {"currencyCode": "USD", "amount": "100"},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_does_not_contain_an_empty_conversion_value_object(self):
|
||||
self.run_function(self._inputs(conversionValue=None, currencyCode=None))
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://api.linkedin.com/rest/conversionEvents",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Bearer oauth-1234",
|
||||
"Content-Type": "application/json",
|
||||
"LinkedIn-Version": "202508",
|
||||
},
|
||||
"body": {
|
||||
"conversion": "urn:lla:llaPartnerConversion:conversion-rule-12345",
|
||||
"conversionHappenedAt": 1737464596570,
|
||||
"user": {
|
||||
"userIds": [
|
||||
{
|
||||
"idType": "SHA256_EMAIL",
|
||||
"idValue": "3edfaed7454eedb3c72bad566901af8bfbed1181816dde6db91dfff0f0cffa98",
|
||||
},
|
||||
{"idType": "LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID", "idValue": "abc"},
|
||||
],
|
||||
"userInfo": {
|
||||
"lastName": "AI",
|
||||
"firstName": "Max",
|
||||
"companyName": "PostHog",
|
||||
"countryCode": "US",
|
||||
},
|
||||
},
|
||||
"eventId": "event-12345",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user