Skip to content

Commit

Permalink
Merge branch 'master' into environments-endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Twixes committed Sep 11, 2024
2 parents 4c080a0 + f1e7601 commit cc4a5f0
Show file tree
Hide file tree
Showing 32 changed files with 1,035 additions and 37 deletions.
Binary file added frontend/public/services/gleap.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: 1 addition & 1 deletion frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2224,7 +2224,7 @@ const api = {
async get(id: IntegrationType['id']): Promise<IntegrationType> {
return await new ApiRequest().integration(id).get()
},
async create(data: Partial<IntegrationType>): Promise<IntegrationType> {
async create(data: Partial<IntegrationType> | FormData): Promise<IntegrationType> {
return await new ApiRequest().integrations().create({ data })
},
async delete(integrationId: IntegrationType['id']): Promise<IntegrationType> {
Expand Down
37 changes: 36 additions & 1 deletion 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 IconGoogleCloud from 'public/services/google-cloud.png'
import IconHubspot from 'public/services/hubspot.png'
import IconSalesforce from 'public/services/salesforce.png'
import IconSlack from 'public/services/slack.png'
Expand All @@ -18,6 +19,7 @@ const ICONS: Record<IntegrationKind, any> = {
slack: IconSlack,
salesforce: IconSalesforce,
hubspot: IconHubspot,
'google-pubsub': IconGoogleCloud,
}

export const integrationsLogic = kea<integrationsLogicType>([
Expand All @@ -28,10 +30,15 @@ export const integrationsLogic = kea<integrationsLogicType>([

actions({
handleOauthCallback: (kind: IntegrationKind, searchParams: any) => ({ kind, searchParams }),
newGoogleCloudKey: (kind: string, key: File, callback?: (integration: IntegrationType) => void) => ({
kind,
key,
callback,
}),
deleteIntegration: (id: number) => ({ id }),
}),

loaders(() => ({
loaders(({ values }) => ({
integrations: [
null as IntegrationType[] | null,
{
Expand All @@ -48,6 +55,34 @@ export const integrationsLogic = kea<integrationsLogicType>([
}
})
},
newGoogleCloudKey: async ({ kind, key, callback }) => {
try {
const formData = new FormData()
formData.append('kind', kind)
formData.append('key', key)
const response = await api.integrations.create(formData)
const responseWithIcon = { ...response, icon_url: ICONS[kind] ?? ICONS['google-pubsub'] }

// run onChange after updating the integrations loader
window.setTimeout(() => callback?.(responseWithIcon), 0)

if (
values.integrations?.find(
(x) => x.kind === kind && x.display_name === response.display_name
)
) {
lemonToast.success('Google Cloud key updated.')
return values.integrations.map((x) =>
x.kind === kind && x.display_name === response.display_name ? responseWithIcon : x
)
}
lemonToast.success('Google Cloud key created.')
return [...(values.integrations ?? []), responseWithIcon]
} catch (e) {
lemonToast.error('Failed to upload Google Cloud key.')
throw e
}
},
},
],
})),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IconExternal, IconX } from '@posthog/icons'
import { LemonButton, LemonMenu, LemonSkeleton } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { useActions, useValues } from 'kea'
import api from 'lib/api'
import { integrationsLogic } from 'lib/integrations/integrationsLogic'
import { IntegrationView } from 'lib/integrations/IntegrationView'
Expand All @@ -21,6 +21,7 @@ export function IntegrationChoice({
redirectUrl,
}: IntegrationConfigureProps): JSX.Element | null {
const { integrationsLoading, integrations } = useValues(integrationsLogic)
const { newGoogleCloudKey } = useActions(integrationsLogic)
const kind = integration
const integrationsOfKind = integrations?.filter((x) => x.kind === kind)
const integrationKind = integrationsOfKind?.find((integration) => integration.id === value)
Expand All @@ -33,6 +34,22 @@ export function IntegrationChoice({
return <LemonSkeleton className="h-10" />
}

const kindName = kind == 'google-pubsub' ? 'Google Cloud Pub/Sub' : capitalizeFirstLetter(kind)

function uploadKey(kind: string): void {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) {
return
}
newGoogleCloudKey(kind, file, (integration) => onChange?.(integration.id))
}
input.click()
}

const button = (
<LemonMenu
items={[
Expand All @@ -48,20 +65,29 @@ export function IntegrationChoice({
],
}
: null,
{
items: [
{
to: api.integrations.authorizeUrl({
kind,
next: redirectUrl,
}),
disableClientSideRouting: true,
label: integrationsOfKind?.length
? `Connect to a different ${kind} integration`
: `Connect to ${kind}`,
},
],
},
kind.startsWith('google-')
? {
items: [
{
onClick: () => uploadKey(kind),
label: 'Upload Google Cloud .json key file',
},
],
}
: {
items: [
{
to: api.integrations.authorizeUrl({
kind,
next: redirectUrl,
}),
disableClientSideRouting: true,
label: integrationsOfKind?.length
? `Connect to a different ${kind} integration`
: `Connect to ${kind}`,
},
],
},
{
items: [
{
Expand All @@ -83,7 +109,7 @@ export function IntegrationChoice({
{integrationKind ? (
<LemonButton type="secondary">Change</LemonButton>
) : (
<LemonButton type="secondary">Choose {capitalizeFirstLetter(kind)} connection</LemonButton>
<LemonButton type="secondary">Choose {kindName} connection</LemonButton>
)}
</LemonMenu>
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3571,7 +3571,7 @@ export enum EventDefinitionType {
EventPostHog = 'event_posthog',
}

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

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: 0467_add_web_vitals_allowed_metrics
posthog: 0468_integration_google_pubsub
sessions: 0001_initial
social_django: 0010_uid_db_index
two_factor: 0007_auto_20201201_1019
5 changes: 4 additions & 1 deletion posthog/api/dashboards/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,10 @@ def move_tile(self, request: Request, *args: Any, **kwargs: Any) -> Response:
parser_classes=[DashboardTemplateCreationJSONSchemaParser],
)
def create_from_template_json(self, request: Request, *args: Any, **kwargs: Any) -> Response:
dashboard = Dashboard.objects.create(team_id=self.team_id)
dashboard = Dashboard.objects.create(
team_id=self.team_id,
created_by=cast(User, request.user),
)

try:
dashboard_template = DashboardTemplate(**request.data["template"])
Expand Down
11 changes: 11 additions & 0 deletions posthog/api/decide.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ def get_decide(request: HttpRequest):
team = user.teams.get(id=project_id)

if team:
if team.id in settings.DECIDE_SHORT_CIRCUITED_TEAM_IDS:
return cors_response(
request,
generate_exception_response(
"decide",
f"Team with ID {team.id} cannot access the /decide endpoint."
f"Please contact us at [email protected]",
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
),
)

token = cast(str, token) # we know it's not None if we found a team
structlog.contextvars.bind_contextvars(team_id=team.id)

Expand Down
16 changes: 14 additions & 2 deletions posthog/api/integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from typing import Any

from django.http import HttpResponse
Expand All @@ -10,7 +12,7 @@

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import UserBasicSerializer
from posthog.models.integration import Integration, OauthIntegration, SlackIntegration
from posthog.models.integration import Integration, OauthIntegration, SlackIntegration, GoogleCloudIntegration


class IntegrationSerializer(serializers.ModelSerializer):
Expand All @@ -27,7 +29,17 @@ def create(self, validated_data: Any) -> Any:
request = self.context["request"]
team_id = self.context["team_id"]

if validated_data["kind"] in OauthIntegration.supported_kinds:
if validated_data["kind"] in GoogleCloudIntegration.supported_kinds:
key_file = request.FILES.get("key")
if not key_file:
raise ValidationError("Key file not provided")
key_info = json.loads(key_file.read().decode("utf-8"))
instance = GoogleCloudIntegration.integration_from_key(
validated_data["kind"], key_info, team_id, request.user
)
return instance

elif validated_data["kind"] in OauthIntegration.supported_kinds:
try:
instance = OauthIntegration.integration_from_oauth_response(
validated_data["kind"], team_id, request.user, validated_data["config"]
Expand Down
2 changes: 2 additions & 0 deletions posthog/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,8 @@ def migrate(self, request: request.Request, **kwargs):
raise ValidationError("No migration available for this plugin")

hog_function_data = migrater.migrate(obj)
hog_function_data["template_id"] = hog_function_data["id"]
del hog_function_data["id"]

if obj.enabled:
hog_function_data["enabled"] = True
Expand Down
3 changes: 3 additions & 0 deletions posthog/api/test/dashboards/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,9 @@ def test_create_from_template_json(self, mock_capture) -> None:

self.assertEqual(dashboard["name"], valid_template["template_name"], dashboard)
self.assertEqual(dashboard["description"], valid_template["dashboard_description"])
self.assertEqual(
dashboard["created_by"], dashboard["created_by"] | {"first_name": "", "email": "[email protected]"}
)

self.assertEqual(len(dashboard["tiles"]), 1)

Expand Down
35 changes: 35 additions & 0 deletions posthog/api/test/test_decide.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Plugin,
PluginConfig,
PluginSourceFile,
Project,
)
from posthog.models.cohort.cohort import Cohort
from posthog.models.feature_flag.feature_flag import FeatureFlagHashKeyOverride
Expand Down Expand Up @@ -2647,6 +2648,40 @@ def test_missing_token(self, *args):
response = self._post_decide({"distinct_id": "example_id", "api_key": None, "project_id": self.team.id})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_short_circuited_team(self, *args):
short_circuited_team_token = "short_circuited_team_token"

_, short_circuited_team = Project.objects.create_with_team(
organization=self.organization,
team_fields={
"api_token": short_circuited_team_token,
"test_account_filters": [
{
"key": "email",
"value": "@posthog.com",
"operator": "not_icontains",
"type": "person",
}
],
"has_completed_onboarding_for": {"product_analytics": True},
},
initiating_user=self.user,
)
with self.settings(DECIDE_SHORT_CIRCUITED_TEAM_IDS=[short_circuited_team.id]):
response = self._post_decide(
{
"distinct_id": "example_id",
"api_key": short_circuited_team_token,
"project_id": short_circuited_team.id,
}
)
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
response_data = response.json()
self.assertEqual(
response_data["detail"],
f"Team with ID {short_circuited_team.id} cannot access the /decide endpoint.Please contact us at [email protected]",
)

def test_invalid_payload_on_decide_endpoint(self, *args):
invalid_payloads = [
base64.b64encode(b"1-1").decode("utf-8"),
Expand Down
5 changes: 5 additions & 0 deletions posthog/cdp/templates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from .avo.template_avo import template as avo
from .loops.template_loops import template as loops
from .rudderstack.template_rudderstack import template as rudderstack
from .gleap.template_gleap import template as gleap
from .google_pubsub.template_google_pubsub import template as google_pubsub, TemplateGooglePubSubMigrator


HOG_FUNCTION_TEMPLATES = [
Expand All @@ -38,6 +40,8 @@
loops,
rudderstack,
avo,
gleap,
google_pubsub,
]


Expand All @@ -47,6 +51,7 @@
TemplateCustomerioMigrator.plugin_url: TemplateCustomerioMigrator,
TemplateIntercomMigrator.plugin_url: TemplateIntercomMigrator,
TemplateSendGridMigrator.plugin_url: TemplateSendGridMigrator,
TemplateGooglePubSubMigrator.plugin_url: TemplateGooglePubSubMigrator,
}

__all__ = ["HOG_FUNCTION_TEMPLATES", "HOG_FUNCTION_TEMPLATES_BY_ID"]
Loading

0 comments on commit cc4a5f0

Please sign in to comment.