-
Notifications
You must be signed in to change notification settings - Fork 562
feat: Mixpanel cohort sync webhook #8338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bd0a549
7dd9c57
f25818f
a34a1ce
b7a2a70
83400d6
10d6468
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Generated by Django 5.2.16 on 2026-08-20 10:06 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("cohorts", "0003_cohort_sync_key"), | ||
| ("environments", "0039_use_no_ssrf_url_field"), | ||
| ("segments", "0032_add_segment_rules_data"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="cohort", | ||
| name="external_id", | ||
| field=models.CharField(blank=True, max_length=255, null=True), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="cohort", | ||
| name="source_type", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("csv", "CSV"), | ||
| ("amplitude", "Amplitude"), | ||
| ("mixpanel", "Mixpanel"), | ||
| ], | ||
| default="csv", | ||
| max_length=50, | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import typing | ||
|
|
||
| from django.db import models | ||
| from rest_framework_api_key.models import AbstractAPIKey | ||
| from rest_framework_api_key.models import AbstractAPIKey, APIKeyManager | ||
|
|
||
| from cohorts.constants import COHORT_SYSTEM_TRAIT_KEY_PREFIX | ||
| from core.models import SoftDeleteExportableModel | ||
|
|
@@ -8,6 +10,7 @@ | |
| class CohortSourceType(models.TextChoices): | ||
| CSV = "csv", "CSV" | ||
| AMPLITUDE = "amplitude", "Amplitude" | ||
| MIXPANEL = "mixpanel", "Mixpanel" | ||
|
|
||
|
|
||
| class Cohort(SoftDeleteExportableModel): | ||
|
|
@@ -26,6 +29,11 @@ class Cohort(SoftDeleteExportableModel): | |
| choices=CohortSourceType.choices, | ||
| default=CohortSourceType.CSV, | ||
| ) | ||
| # The cohort's identifier in the external source. Mixpanel pushes under | ||
| # its own cohort ID, so we store it to route later requests; Amplitude | ||
| # uses the ID we hand back at list creation, and CSV cohorts have no | ||
| # external system, so both leave this null. | ||
| external_id = models.CharField(max_length=255, null=True, blank=True) | ||
|
Comment on lines
+32
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Make active cohort lookup and creation atomic. Concurrent initial Add a partial unique constraint for active cohorts with non-null external IDs, and use an atomic get-or-create flow that re-fetches after an integrity conflict. 📍 Affects 2 files
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deliberately dropped to keep v1 lean — we'll add the constraint and conflict recovery if duplicate creation is observed in practice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to create a follow-up GitHub issue for the active-cohort uniqueness constraint and atomic conflict recovery? 🐇 ✏️ Learnings added
You are interacting with an AI system. |
||
| version = models.PositiveIntegerField(default=0) | ||
| created_at = models.DateTimeField(auto_now_add=True) | ||
| # Deletion drains memberships from the identity store first; the cohort is | ||
|
|
@@ -48,7 +56,19 @@ class Meta: | |
| ] | ||
|
|
||
|
|
||
| class CohortSyncKeyManager(APIKeyManager): | ||
| def get_from_key(self, key: str) -> "CohortSyncKey": | ||
| if "\x00" in key: | ||
| # A NUL can't travel in a raw header, but base64 credentials can | ||
| # decode to one, and the database driver refuses to build a query | ||
| # containing it. No real key holds one, so treat it as absent. | ||
| raise self.model.DoesNotExist("Key contains a NUL character.") | ||
| return typing.cast("CohortSyncKey", super().get_from_key(key)) | ||
|
|
||
|
|
||
| class CohortSyncKey(AbstractAPIKey): | ||
| objects: typing.ClassVar[CohortSyncKeyManager] = CohortSyncKeyManager() | ||
|
|
||
| environment = models.ForeignKey( | ||
| "environments.Environment", | ||
| on_delete=models.CASCADE, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,15 @@ | ||
| from django.urls import path | ||
| from rest_framework.routers import SimpleRouter | ||
|
|
||
| from cohorts.sync_views import AmplitudeCohortSyncViewSet | ||
| from cohorts.sync_views import AmplitudeCohortSyncViewSet, MixpanelCohortSyncView | ||
|
|
||
| app_name = "cohort-sync" | ||
|
|
||
| # SimpleRouter: nothing here is browsed by a person. | ||
| router = SimpleRouter() | ||
| router.register(r"amplitude/lists", AmplitudeCohortSyncViewSet, basename="amplitude") | ||
|
|
||
| urlpatterns = router.urls | ||
| urlpatterns = [ | ||
| path("mixpanel/webhook/", MixpanelCohortSyncView.as_view(), name="mixpanel"), | ||
| *router.urls, | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: Flagsmith/flagsmith
Length of output: 885
🏁 Script executed:
Repository: Flagsmith/flagsmith
Length of output: 4676
🏁 Script executed:
Repository: Flagsmith/flagsmith
Length of output: 485
🌐 Web query:
drf-spectacular 0.28.0 OpenApiAuthenticationExtension name list get_security_definition implementation OpenAPI component key pattern 3.0.3💡 Result:
In drf-spectacular 0.28.0, the OpenApiAuthenticationExtension allows you to define authentication schemes that are registered in the OpenAPI components section [1][2]. Implementation for Single vs. Multiple Security Definitions: To implement a single security definition, set the name attribute to a string and return a dictionary from get_security_definition [1][3]. For scenarios requiring multiple parameters or a group of headers (e.g., to model security requirements that include multiple headers), you should define name as a list of strings and return a corresponding list of dictionary definitions from get_security_definition [1][2][4]. Example Implementation: class MyCustomAuthExtension(OpenApiAuthenticationExtension): target_class = 'path.to.YourAuthenticationClass' name = ['HeaderOne', 'HeaderTwo'] def get_security_definition(self, auto_schema): return [ {'type': 'apiKey', 'in': 'header', 'name': 'X-Header-One'}, {'type': 'apiKey', 'in': 'header', 'name': 'X-Header-Two'} ] OpenAPI Component Key Pattern: When drf-spectacular processes these extensions, it registers them under the securitySchemes component in the OpenAPI specification [5][6]. Under OpenAPI 3.0.3, these components are structured under the components/securitySchemes key [6]. The library ensures that each name provided in your extension is registered as a unique component key within the resulting OpenAPI schema [5]. If you provide a list of names, the library iterates through them and maps each to the corresponding definition returned by get_security_definition, ensuring they are correctly placed in the schema components [5]. Key Technical Details: - The get_security_definition method is expected to return a dictionary (or a list of dictionaries) compliant with the OpenAPI 3.0.3 Security Scheme Object specification [1][3]. - Internally, drf-spectacular uses the ResolvedComponent class to handle these, where the security component type is defined as securitySchemes [6]. - The library automatically warns if it detects component naming collisions [6]. For most standard implementations, refer to the official documentation on workflow and customization [3][7].
Citations:
🌐 Web query:
OpenAPI 3.0.3 specification components securitySchemes map keys pattern ^[a-zA-Z0-9.-_]+$💡 Result:
In the OpenAPI 3.0.3 specification, the keys used within the components/securitySchemes object must conform to the regular expression ^[a-zA-Z0-9.-_]+$ [1][2][3]. This requirement applies generally to all fixed fields declared within the components object (such as schemas, responses, parameters, examples, requestBodies, headers, links, and callbacks) [2][3]. The specification explicitly mandates that the names used as keys in these maps must match this pattern [1][4]. While these names are arbitrary and used to refer to the security schemes from other parts of the API definition (such as the global or operation-level security requirements), they must adhere to the specified character constraints to be compliant with the OpenAPI 3.0.3 standard [5]. Note that while the textual specification imposes this constraint, it has been noted in community discussions that the corresponding JSON schema files for version 3.0 historically lacked the automated validation (such as additionalProperties: false) to strictly enforce this pattern, which was considered an oversight in the 3.0 schema design [6].
Citations:
Use OpenAPI-safe names for the security schemes.
CohortSyncKeyAuthenticationExtension.namegenerates thecomponents.securitySchemeskeys. The current names contain spaces and parentheses, which violate the OpenAPI 3.0.3 component-key pattern. The invalid names are also present inopenapi.yamlandsdk/openapi.yaml. Rename both names and regenerate these documents.Source: MCP tools