mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat(cdp): add Userlist integration template and tests (#39407)
Forcing in as the failing tests are unrelated
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
BIN
frontend/public/services/userlist.png
Normal file
BIN
frontend/public/services/userlist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -952,12 +952,6 @@ const destinationDefinitions: DestinationConfig[] = [
|
||||
icon_url: '/static/coming-soon-destinations/Tune.svg',
|
||||
category: ['Marketing'],
|
||||
},
|
||||
{
|
||||
name: 'Userlist',
|
||||
id: 'coming-soon-userlist',
|
||||
icon_url: '/static/coming-soon-destinations/Userlist.png',
|
||||
category: ['Marketing'],
|
||||
},
|
||||
{
|
||||
name: 'Vero',
|
||||
id: 'coming-soon-vero',
|
||||
|
||||
@@ -82,6 +82,7 @@ from .sendgrid.template_sendgrid import (
|
||||
from .slack.template_slack import template as slack
|
||||
from .snapchat_ads.template_pixel import template_snapchat_pixel as snapchat_pixel
|
||||
from .tiktok_ads.template_tiktok_pixel import template_tiktok_pixel as tiktok_pixel
|
||||
from .userlist.template_userlist import template as userlist
|
||||
from .zapier.template_zapier import template as zapier
|
||||
from .zendesk.template_zendesk import template as zendesk
|
||||
|
||||
@@ -128,6 +129,7 @@ HOG_FUNCTION_TEMPLATES = [
|
||||
sendgrid,
|
||||
snapchat_pixel,
|
||||
tiktok_pixel,
|
||||
userlist,
|
||||
zapier,
|
||||
zendesk,
|
||||
early_access_features,
|
||||
|
||||
165
posthog/cdp/templates/userlist/template_userlist.py
Normal file
165
posthog/cdp/templates/userlist/template_userlist.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from posthog.cdp.templates.hog_function_template import HogFunctionTemplateDC
|
||||
|
||||
template: HogFunctionTemplateDC = HogFunctionTemplateDC(
|
||||
status="beta",
|
||||
free=False,
|
||||
type="destination",
|
||||
id="template-userlist",
|
||||
name="Userlist",
|
||||
description="Send user, company, and event data to Userlist",
|
||||
icon_url="/static/services/userlist.png",
|
||||
category=["Email Marketing"],
|
||||
code_language="hog",
|
||||
code="""
|
||||
let base_uri := 'https://incoming.userlist.com/posthog'
|
||||
|
||||
fun compact(obj) {
|
||||
let result := {}
|
||||
|
||||
for (let key, value in obj) {
|
||||
if (value != null) {
|
||||
result[key] := value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun push(endpoint, body) {
|
||||
if (empty(body)) {
|
||||
print('Error sending data to Userlist: Invalid payload')
|
||||
return
|
||||
}
|
||||
|
||||
let res := fetch(f'{base_uri}{endpoint}', {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': f'Push {inputs.push_key}',
|
||||
},
|
||||
'body': body
|
||||
})
|
||||
|
||||
if (res.status >= 400) {
|
||||
print(f'Error sending data to Userlist: {res.status} - {res.body}')
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
let user_payload := compact({
|
||||
'identifier': inputs.user_identifier,
|
||||
'email': inputs.user_email,
|
||||
'properties': compact(inputs.user_properties)
|
||||
})
|
||||
|
||||
if (empty(user_payload.identifier) and empty(user_payload.email)) {
|
||||
user_payload := null
|
||||
}
|
||||
|
||||
let company_payload := compact({
|
||||
'identifier': inputs.company_identifier,
|
||||
'name': inputs.company_name,
|
||||
'properties': compact(inputs.company_properties),
|
||||
})
|
||||
|
||||
if (empty(company_payload.identifier)) {
|
||||
company_payload := null
|
||||
}
|
||||
|
||||
let event_payload := compact({
|
||||
'name': event.event,
|
||||
'user': user_payload,
|
||||
'company': company_payload,
|
||||
'occurred_at': event.timestamp,
|
||||
'properties': compact(event.properties)
|
||||
})
|
||||
|
||||
if (empty(event_payload.name) or (empty(event_payload.user) and empty(event_payload.company))) {
|
||||
event_payload := null
|
||||
}
|
||||
|
||||
if (event.event in ['$identify', '$set']) {
|
||||
push('/users', user_payload)
|
||||
} else if (event.event == '$groupidentify') {
|
||||
if (not empty(company_payload) and not empty(user_payload)) {
|
||||
company_payload.user := user_payload
|
||||
}
|
||||
|
||||
push('/companies', company_payload)
|
||||
} else if (match(event.event, '^[a-z][a-z0-9_-]*$')) {
|
||||
push('/events', event_payload)
|
||||
} else {
|
||||
print(f'Skipping event {event.event} as it is not supported.')
|
||||
return
|
||||
}
|
||||
""".strip(),
|
||||
inputs_schema=[
|
||||
{
|
||||
"key": "push_key",
|
||||
"type": "string",
|
||||
"label": "Push Key",
|
||||
"description": "You can find your Push Key in your [Userlist Push settings](https://app.userlist.com/settings/push)",
|
||||
"secret": True,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "user_identifier",
|
||||
"type": "string",
|
||||
"label": "User Identifier",
|
||||
"description": "The unique identifier for the user in Userlist.",
|
||||
"default": "{person.id}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "user_email",
|
||||
"type": "string",
|
||||
"label": "User Email",
|
||||
"description": "The email address of the user.",
|
||||
"default": "{person.properties.email}",
|
||||
},
|
||||
{
|
||||
"key": "user_properties",
|
||||
"type": "dictionary",
|
||||
"label": "Custom User Properties",
|
||||
"description": "Map of custom user properties and their values.",
|
||||
"default": {
|
||||
"lastname": "{person.properties.lastname ?? person.properties.lastName ?? person.properties.last_name}",
|
||||
"firstname": "{person.properties.firstname ?? person.properties.firstName ?? person.properties.first_name}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "company_identifier",
|
||||
"type": "string",
|
||||
"label": "Company Identifier",
|
||||
"description": "The unique identifier for the company in Userlist.",
|
||||
"default": "{groups.account.id}",
|
||||
},
|
||||
{
|
||||
"key": "company_name",
|
||||
"type": "string",
|
||||
"label": "Company Name",
|
||||
"description": "The name of the company.",
|
||||
"default": "{groups.account.properties.name}",
|
||||
},
|
||||
{
|
||||
"key": "company_properties",
|
||||
"type": "dictionary",
|
||||
"label": "Custom Company Properties",
|
||||
"description": "Map of custom company properties and their values.",
|
||||
"default": {
|
||||
"industry": "{groups.account.properties.industry}",
|
||||
},
|
||||
},
|
||||
],
|
||||
filters={
|
||||
"events": [
|
||||
{"id": "$identify", "name": "$identify", "type": "events", "order": 0},
|
||||
{"id": "$set", "name": "$set", "type": "events", "order": 1},
|
||||
{"id": "$groupidentify", "name": "$groupidentify", "type": "events", "order": 2},
|
||||
],
|
||||
"actions": [],
|
||||
"filter_test_accounts": True,
|
||||
},
|
||||
)
|
||||
304
posthog/cdp/templates/userlist/test_template_userlist.py
Normal file
304
posthog/cdp/templates/userlist/test_template_userlist.py
Normal file
@@ -0,0 +1,304 @@
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from posthog.cdp.templates.helpers import BaseHogFunctionTemplateTest
|
||||
from posthog.cdp.templates.userlist.template_userlist import template as template_userlist
|
||||
|
||||
|
||||
def create_inputs(**kwargs):
|
||||
inputs = {
|
||||
"push_key": "test_push_key",
|
||||
"user_identifier": "user_123",
|
||||
"user_email": "user@example.com",
|
||||
"user_properties": {"first_name": "John", "last_name": "Doe"},
|
||||
"company_identifier": None,
|
||||
"company_name": None,
|
||||
"company_properties": {"industry": None},
|
||||
}
|
||||
inputs.update(kwargs)
|
||||
return inputs
|
||||
|
||||
|
||||
class TestTemplateUserlist(BaseHogFunctionTemplateTest):
|
||||
template = template_userlist
|
||||
|
||||
def test_identify_event(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(),
|
||||
globals={
|
||||
"event": {"event": "$identify"},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://incoming.userlist.com/posthog/users",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Push test_push_key",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
"body": {
|
||||
"email": "user@example.com",
|
||||
"identifier": "user_123",
|
||||
"properties": {"first_name": "John", "last_name": "Doe"},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_set_event(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
user_identifier="user_456",
|
||||
user_email="updated@example.com",
|
||||
user_properties={"first_name": "Jane", "last_name": "Smith"},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "$set"},
|
||||
"person": {
|
||||
"id": "user_456",
|
||||
"properties": {"email": "updated@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://incoming.userlist.com/posthog/users",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Push test_push_key",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
"body": {
|
||||
"email": "updated@example.com",
|
||||
"identifier": "user_456",
|
||||
"properties": {"first_name": "Jane", "last_name": "Smith"},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_groupidentify_event(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
company_identifier="company_123",
|
||||
company_name="Acme Corp",
|
||||
company_properties={"industry": "Technology", "employee_count": "50"},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "$groupidentify"},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://incoming.userlist.com/posthog/companies",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Push test_push_key",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
"body": {
|
||||
"identifier": "company_123",
|
||||
"name": "Acme Corp",
|
||||
"properties": {"industry": "Technology", "employee_count": "50"},
|
||||
"user": {
|
||||
"identifier": "user_123",
|
||||
"email": "user@example.com",
|
||||
"properties": {"first_name": "John", "last_name": "Doe"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_custom_event(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
company_identifier="company_123",
|
||||
company_name="Acme Corp",
|
||||
company_properties={"industry": "Technology"},
|
||||
),
|
||||
globals={
|
||||
"event": {
|
||||
"event": "button_clicked",
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"properties": {"button_name": "signup", "page": "homepage"},
|
||||
},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://incoming.userlist.com/posthog/events",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Push test_push_key",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
"body": {
|
||||
"name": "button_clicked",
|
||||
"user": {
|
||||
"identifier": "user_123",
|
||||
"email": "user@example.com",
|
||||
"properties": {"first_name": "John", "last_name": "Doe"},
|
||||
},
|
||||
"company": {
|
||||
"identifier": "company_123",
|
||||
"name": "Acme Corp",
|
||||
"properties": {"industry": "Technology"},
|
||||
},
|
||||
"occurred_at": "2024-01-01T00:00:00Z",
|
||||
"properties": {"button_name": "signup", "page": "homepage"},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_compact_removes_null_values(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
user_email=None,
|
||||
user_properties={"first_name": "John", "last_name": None, "middle_name": None},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "$identify"},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls()[0] == snapshot(
|
||||
(
|
||||
"https://incoming.userlist.com/posthog/users",
|
||||
{
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Push test_push_key",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
"body": {
|
||||
"identifier": "user_123",
|
||||
"properties": {"first_name": "John"},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_user_payload_without_email_and_identifier_is_not_sent(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
user_identifier=None,
|
||||
user_email=None,
|
||||
user_properties={"first_name": "John"},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "$identify"},
|
||||
"person": {
|
||||
"id": None,
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls() == []
|
||||
assert self.get_mock_print_calls() == snapshot([("Error sending data to Userlist: Invalid payload",)])
|
||||
|
||||
def test_company_payload_without_identifier_is_not_sent(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
user_properties={"first_name": "John"},
|
||||
company_name="Acme Corp",
|
||||
company_properties={"industry": "Technology"},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "$groupidentify"},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls() == []
|
||||
assert self.get_mock_print_calls() == snapshot([("Error sending data to Userlist: Invalid payload",)])
|
||||
|
||||
def test_event_payload_without_user_and_company_is_not_sent(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(
|
||||
user_identifier=None,
|
||||
user_email=None,
|
||||
user_properties={"first_name": "John"},
|
||||
company_properties={"industry": "Technology"},
|
||||
),
|
||||
globals={
|
||||
"event": {"event": "custom_event", "timestamp": "2024-01-01T00:00:00Z", "properties": {}},
|
||||
"person": {
|
||||
"id": None,
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls() == []
|
||||
assert self.get_mock_print_calls() == snapshot([("Error sending data to Userlist: Invalid payload",)])
|
||||
|
||||
def test_function_prints_error_on_bad_status(self):
|
||||
self.mock_fetch_response = lambda *args: {"status": 400, "body": {"error": "Invalid request"}} # type: ignore
|
||||
self.run_function(
|
||||
inputs=create_inputs(),
|
||||
globals={
|
||||
"event": {"event": "$identify"},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_print_calls() == snapshot(
|
||||
[("Error sending data to Userlist: 400 - {'error': 'Invalid request'}",)]
|
||||
)
|
||||
|
||||
def test_system_events_are_skipped(self):
|
||||
self.run_function(
|
||||
inputs=create_inputs(),
|
||||
globals={
|
||||
"event": {
|
||||
"event": "$pageview",
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"properties": {"url": "https://example.com"},
|
||||
},
|
||||
"person": {
|
||||
"id": "user_123",
|
||||
"properties": {"email": "user@example.com"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert self.get_mock_fetch_calls() == []
|
||||
assert self.get_mock_print_calls() == snapshot([("Skipping event $pageview as it is not supported.",)])
|
||||
Reference in New Issue
Block a user