Skip to content

Commit

Permalink
feat(cdp): Add google ads hog function (#25530)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarconLP authored Oct 15, 2024
1 parent 69f7e9a commit 35f3fb3
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 4 deletions.
Binary file added frontend/public/services/google-ads.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/src/lib/integrations/integrationsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { loaders } from 'kea-loaders'
import { router, urlToAction } from 'kea-router'
import api from 'lib/api'
import { fromParamsGivenUrl } from 'lib/utils'
import IconGoogleAds from 'public/services/google-ads.png'
import IconGoogleCloud from 'public/services/google-cloud.png'
import IconGoogleCloudStorage from 'public/services/google-cloud-storage.png'
import IconHubspot from 'public/services/hubspot.png'
Expand All @@ -22,6 +23,7 @@ const ICONS: Record<IntegrationKind, any> = {
hubspot: IconHubspot,
'google-pubsub': IconGoogleCloud,
'google-cloud-storage': IconGoogleCloudStorage,
'google-ads': IconGoogleAds,
}

export const integrationsLogic = kea<integrationsLogicType>([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export function IntegrationChoice({
? 'Google Cloud Pub/Sub'
: kind == 'google-cloud-storage'
? 'Google Cloud Storage'
: kind == 'google-ads'
? 'Google Ads'
: capitalizeFirstLetter(kind)

function uploadKey(kind: string): void {
Expand Down Expand Up @@ -72,7 +74,7 @@ export function IntegrationChoice({
],
}
: null,
kind.startsWith('google-')
['google-pubsub', 'google-cloud-storage'].includes(kind)
? {
items: [
{
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3654,7 +3654,13 @@ export enum EventDefinitionType {
EventPostHog = 'event_posthog',
}

export type IntegrationKind = 'slack' | 'salesforce' | 'hubspot' | 'google-pubsub' | 'google-cloud-storage'
export type IntegrationKind =
| 'slack'
| 'salesforce'
| 'hubspot'
| 'google-pubsub'
| 'google-cloud-storage'
| 'google-ads'

export interface IntegrationType {
id: number
Expand Down
2 changes: 1 addition & 1 deletion latest_migrations.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name
ee: 0016_rolemembership_organization_member
otp_static: 0002_throttling
otp_totp: 0002_auto_20190420_0723
posthog: 0488_alter_user_is_active
posthog: 0489_alter_integration_kind
sessions: 0001_initial
social_django: 0010_uid_db_index
two_factor: 0007_auto_20201201_1019
2 changes: 2 additions & 0 deletions posthog/cdp/templates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .knock.template_knock import template as knock
from .meta_ads.template_meta_ads import template as meta_ads
from .activecampaign.template_activecampaign import template as activecampaign
from .google_ads.template_google_ads import template as google_ads
from .attio.template_attio import template as attio
from .google_cloud_storage.template_google_cloud_storage import (
template as google_cloud_storage,
Expand All @@ -44,6 +45,7 @@
customerio,
engage,
gleap,
google_ads,
google_cloud_storage,
google_pubsub,
hubspot,
Expand Down
93 changes: 93 additions & 0 deletions posthog/cdp/templates/google_ads/template_google_ads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from posthog.cdp.templates.hog_function_template import HogFunctionTemplate

template: HogFunctionTemplate = HogFunctionTemplate(
status="alpha",
id="template-google-ads",
name="Google Ads Conversions",
description="Send conversion events to Google Ads",
icon_url="/static/services/google-ads.png",
category=["Advertisement"],
hog="""
let res := fetch(f'https://googleads.googleapis.com/v17/customers/{replaceAll(inputs.customerId, '-', '')}:uploadClickConversions', {
'method': 'POST',
'headers': {
'Authorization': f'Bearer {inputs.oauth.access_token}',
'Content-Type': 'application/json',
'developer-token': inputs.developerToken
},
'body': {
'conversions': [
{
'gclid': inputs.gclid,
'conversionAction': f'customers/{replaceAll(inputs.customerId, '-', '')}/conversionActions/{replaceAll(inputs.conversionActionId, 'AW-', '')}',
'conversionDateTime': inputs.conversionDateTime
}
],
'partialFailure': true,
'validateOnly': true
}
})
if (res.status >= 400) {
throw Error(f'Error from googleads.googleapis.com (status {res.status}): {res.body}')
}
""".strip(),
inputs_schema=[
{
"key": "oauth",
"type": "integration",
"integration": "google-ads",
"label": "Google Ads account",
"secret": False,
"required": True,
},
{
"key": "developerToken",
"type": "string",
"label": "Developer token",
"description": "This should be a 22-character long alphanumeric string. Check out this page on how to obtain such a token: https://developers.google.com/google-ads/api/docs/get-started/dev-token",
"secret": False,
"required": True,
},
{
"key": "customerId",
"type": "string",
"label": "Customer ID",
"description": "ID of your Google Ads Account. This should be 10-digits and in XXX-XXX-XXXX format.",
"secret": False,
"required": True,
},
{
"key": "conversionActionId",
"type": "string",
"label": "Conversion action ID",
"description": "You will find this information in the event snippet for your conversion action, for example send_to: AW-CONVERSION_ID/AW-CONVERSION_LABEL. This should be in the AW-CONVERSION_ID format.",
"secret": False,
"required": True,
},
{
"key": "gclid",
"type": "string",
"label": "Google Click ID (gclid)",
"description": "The Google click ID (gclid) associated with this conversion.",
"default": "{person.gclid}",
"secret": False,
"required": True,
},
{
"key": "conversionDateTime",
"type": "string",
"label": "Conversion Date Time",
"description": 'The date time at which the conversion occurred. Must be after the click time. The timezone must be specified. The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. "2019-01-01 12:32:45-08:00".',
"default": "{event.timestamp}",
"secret": False,
"required": True,
},
],
filters={
"events": [],
"actions": [],
"filter_test_accounts": True,
},
)
51 changes: 51 additions & 0 deletions posthog/cdp/templates/google_ads/test_template_google_ads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from inline_snapshot import snapshot
from posthog.cdp.templates.helpers import BaseHogFunctionTemplateTest
from posthog.cdp.templates.google_ads.template_google_ads import (
template as template_google_ads,
)


class TestTemplateGoogleAds(BaseHogFunctionTemplateTest):
template = template_google_ads

def _inputs(self, **kwargs):
inputs = {
"oauth": {
"access_token": "oauth-1234",
},
"developerToken": "developer-token1234",
"customerId": "123-123-1234",
"conversionActionId": "AW-123456789",
"gclid": "89y4thuergnjkd34oihroh3uhg39uwhgt9",
"conversionDateTime": "2024-10-10 16:32:45+02:00",
}
inputs.update(kwargs)
return inputs

def test_function_works(self):
self.mock_fetch_response = lambda *args: {"status": 200, "body": {"ok": True}} # type: ignore
self.run_function(self._inputs())
assert self.get_mock_fetch_calls()[0] == snapshot(
(
"https://googleads.googleapis.com/v17/customers/1231231234:uploadClickConversions",
{
"body": {
"conversions": [
{
"gclid": "89y4thuergnjkd34oihroh3uhg39uwhgt9",
"conversionAction": f"customers/1231231234/conversionActions/123456789",
"conversionDateTime": "2024-10-10 16:32:45+02:00",
}
],
"partialFailure": True,
"validateOnly": True,
},
"method": "POST",
"headers": {
"Authorization": "Bearer oauth-1234",
"Content-Type": "application/json",
"developer-token": "developer-token1234",
},
},
)
)
27 changes: 27 additions & 0 deletions posthog/migrations/0489_alter_integration_kind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.15 on 2024-10-15 10:30

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("posthog", "0488_alter_user_is_active"),
]

operations = [
migrations.AlterField(
model_name="integration",
name="kind",
field=models.CharField(
choices=[
("slack", "Slack"),
("salesforce", "Salesforce"),
("hubspot", "Hubspot"),
("google-pubsub", "Google Pubsub"),
("google-cloud-storage", "Google Cloud Storage"),
("google-ads", "Google Ads"),
],
max_length=20,
),
),
]
22 changes: 21 additions & 1 deletion posthog/models/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class IntegrationKind(models.TextChoices):
HUBSPOT = "hubspot"
GOOGLE_PUBSUB = "google-pubsub"
GOOGLE_CLOUD_STORAGE = "google-cloud-storage"
GOOGLE_ADS = "google-ads"

team = models.ForeignKey("Team", on_delete=models.CASCADE)

Expand Down Expand Up @@ -107,10 +108,11 @@ class OauthConfig:
name_path: str
token_info_url: Optional[str] = None
token_info_config_fields: Optional[list[str]] = None
additional_authorize_params: Optional[dict[str, str]] = None


class OauthIntegration:
supported_kinds = ["slack", "salesforce", "hubspot"]
supported_kinds = ["slack", "salesforce", "hubspot", "google-ads"]
integration: Integration

def __init__(self, integration: Integration) -> None:
Expand Down Expand Up @@ -168,6 +170,23 @@ def oauth_config_for_kind(cls, kind: str) -> OauthConfig:
id_path="hub_id",
name_path="hub_domain",
)
elif kind == "google-ads":
if not settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY or not settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET:
raise NotImplementedError("Google Ads app not configured")

return OauthConfig(
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
# forces the consent screen, otherwise we won't receive a refresh token
additional_authorize_params={"access_type": "offline", "prompt": "consent"},
token_info_url="https://openidconnect.googleapis.com/v1/userinfo",
token_info_config_fields=["sub", "email"],
token_url="https://oauth2.googleapis.com/token",
client_id=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY,
client_secret=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET,
scope="https://www.googleapis.com/auth/adwords email",
id_path="sub",
name_path="email",
)

raise NotImplementedError(f"Oauth config for kind {kind} not implemented")

Expand All @@ -186,6 +205,7 @@ def authorize_url(cls, kind: str, next="") -> str:
"redirect_uri": cls.redirect_uri(kind),
"response_type": "code",
"state": urlencode({"next": next}),
**(oauth_config.additional_authorize_params or {}),
}

return f"{oauth_config.authorize_url}?{urlencode(query_params)}"
Expand Down
10 changes: 10 additions & 0 deletions posthog/models/test/test_integration_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class TestOauthIntegrationModel(BaseTest):
"SALESFORCE_CONSUMER_SECRET": "salesforce-client-secret",
"HUBSPOT_APP_CLIENT_ID": "hubspot-client-id",
"HUBSPOT_APP_CLIENT_SECRET": "hubspot-client-secret",
"SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "google-client-id",
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "google-client-secret",
}

def create_integration(
Expand All @@ -113,6 +115,14 @@ def test_authorize_url(self):
== "https://login.salesforce.com/services/oauth2/authorize?client_id=salesforce-client-id&scope=full+refresh_token&redirect_uri=https%3A%2F%2Flocalhost%3A8000%2Fintegrations%2Fsalesforce%2Fcallback&response_type=code&state=next%3D%252Fprojects%252Ftest"
)

def test_authorize_url_with_additional_authorize_params(self):
with self.settings(**self.mock_settings):
url = OauthIntegration.authorize_url("google-ads", next="/projects/test")
assert (
url
== "https://accounts.google.com/o/oauth2/v2/auth?client_id=google-client-id&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fadwords+email&redirect_uri=https%3A%2F%2Flocalhost%3A8000%2Fintegrations%2Fgoogle-ads%2Fcallback&response_type=code&state=next%3D%252Fprojects%252Ftest&access_type=offline&prompt=consent"
)

@patch("posthog.models.integration.requests.post")
def test_integration_from_oauth_response(self, mock_post):
with self.settings(**self.mock_settings):
Expand Down

0 comments on commit 35f3fb3

Please sign in to comment.