Skip to content

Commit f16c6d5

Browse files
oliverb123github-actions[bot]Twixes
authored
feat(sig): error tracking as a source (#51645)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Michael Matloka <michael@matloka.com>
1 parent a567f78 commit f16c6d5

File tree

23 files changed

+601
-50
lines changed

23 files changed

+601
-50
lines changed

bin/start-rust-service

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ case "$RS_SVC" in
102102
export ALLOW_INTERNAL_IPS="true"
103103
export PERSONS_URL=postgres://posthog:posthog@localhost:5432/posthog_persons
104104
export ISSUE_BUCKETS_REDIS_URL=redis://localhost:6379
105+
export SIGNALS_API_BASE_URL=http://localhost:8000 # Used to emit signals, via the internal API
106+
export INTERNAL_API_SECRET=posthog123
105107
;;
106108

107109
embedding-worker)

frontend/src/queries/schema.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/queries/schema/schema-signals.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export interface LinearIssueSignalInput {
118118
extra: LinearIssueSignalExtra
119119
}
120120

121+
// Error tracking
122+
123+
export interface ErrorTrackingSignalExtra {
124+
fingerprint: string
125+
}
126+
127+
export interface ErrorTrackingSignalInput {
128+
source_type: 'issue_created' | 'issue_reopened' | 'issue_spiking'
129+
source_product: 'error_tracking'
130+
source_id: string
131+
description: string
132+
weight: number
133+
extra: ErrorTrackingSignalExtra
134+
}
135+
121136
// Discriminated union over all signal variants
122137

123138
/** @discriminator source_product */
@@ -127,3 +142,4 @@ export type SignalInput =
127142
| ZendeskTicketSignalInput
128143
| GithubIssueSignalInput
129144
| LinearIssueSignalInput
145+
| ErrorTrackingSignalInput

posthog/schema.py

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

posthog/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
from products.early_access_features.backend.api import early_access_features
5050
from products.product_tours.backend.api import product_tours
51+
from products.signals.backend import views as signals_views
5152
from products.slack_app.backend.api import (
5253
posthog_code_event_handler,
5354
posthog_code_interactivity_handler,
@@ -247,6 +248,10 @@ def authorize_and_redirect(request: HttpRequest) -> HttpResponse:
247248
"api/projects/<str:team_id>/internal/hog_flows/user_blast_radius_persons",
248249
csrf_exempt(hog_flow.InternalHogFlowViewSet.as_view({"post": "internal_user_blast_radius_persons"})),
249250
),
251+
path(
252+
"api/projects/<str:team_id>/internal/signals/emit",
253+
csrf_exempt(signals_views.InternalSignalViewSet.as_view({"post": "emit"})),
254+
),
250255
# Test setup endpoint (only available in TEST mode)
251256
path("api/setup_test/<str:test_name>/", csrf_exempt(playwright_setup.setup_test)),
252257
re_path(r"^api.+", api_not_found),

products/signals/backend/api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from posthog.sync import database_sync_to_async
1212
from posthog.temporal.common.client import async_connect
1313

14+
from products.signals.backend.models import SignalSourceConfig
1415
from products.signals.backend.temporal.buffer import BufferSignalsWorkflow
1516
from products.signals.backend.temporal.emitter import SignalEmitterInput, SignalEmitterWorkflow
1617
from products.signals.backend.temporal.types import BufferSignalsInput, EmitSignalInputs
@@ -60,6 +61,12 @@ async def emit_signal(
6061
if not organization.is_ai_data_processing_approved:
6162
return
6263

64+
is_enabled = await database_sync_to_async(SignalSourceConfig.is_source_enabled, thread_sensitive=False)(
65+
team.id, source_product, source_type
66+
)
67+
if not is_enabled:
68+
return
69+
6370
token_count = len(_tiktoken_encoding.encode(description))
6471
if token_count > MAX_SIGNAL_DESCRIPTION_TOKENS:
6572
raise ValueError(
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.2.29 on 2026-03-19 15:14
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("signals", "0010_add_data_import_signal_source_choices"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="signalsourceconfig",
14+
name="source_product",
15+
field=models.CharField(
16+
choices=[
17+
("session_replay", "Session replay"),
18+
("llm_analytics", "LLM analytics"),
19+
("github", "GitHub"),
20+
("linear", "Linear"),
21+
("zendesk", "Zendesk"),
22+
("error_tracking", "Error tracking"),
23+
],
24+
max_length=100,
25+
),
26+
),
27+
migrations.AlterField(
28+
model_name="signalsourceconfig",
29+
name="source_type",
30+
field=models.CharField(
31+
choices=[
32+
("session_analysis_cluster", "Session analysis cluster"),
33+
("evaluation", "Evaluation"),
34+
("issue", "Issue"),
35+
("ticket", "Ticket"),
36+
("issue_created", "Issue created"),
37+
("issue_reopened", "Issue reopened"),
38+
("issue_spiking", "Issue spiking"),
39+
],
40+
max_length=100,
41+
),
42+
),
43+
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0010_add_data_import_signal_source_choices
1+
0011_add_error_tracking_signal_types

products/signals/backend/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ class SourceProduct(models.TextChoices):
1313
GITHUB = "github", "GitHub"
1414
LINEAR = "linear", "Linear"
1515
ZENDESK = "zendesk", "Zendesk"
16+
ERROR_TRACKING = "error_tracking", "Error tracking"
1617

1718
class SourceType(models.TextChoices):
1819
SESSION_ANALYSIS_CLUSTER = "session_analysis_cluster", "Session analysis cluster"
1920
EVALUATION = "evaluation", "Evaluation"
2021
ISSUE = "issue", "Issue"
2122
TICKET = "ticket", "Ticket"
23+
ISSUE_CREATED = "issue_created", "Issue created"
24+
ISSUE_REOPENED = "issue_reopened", "Issue reopened"
25+
ISSUE_SPIKING = "issue_spiking", "Issue spiking"
2226

2327
team = models.ForeignKey("posthog.Team", on_delete=models.CASCADE, related_name="signal_source_configs")
2428
source_product = models.CharField(max_length=100, choices=SourceProduct.choices)
@@ -29,6 +33,23 @@ class SourceType(models.TextChoices):
2933
updated_at = models.DateTimeField(auto_now=True)
3034
created_by = models.ForeignKey("posthog.User", on_delete=models.SET_NULL, null=True, blank=True)
3135

36+
@classmethod
37+
def is_source_enabled(cls, team_id: int, source_product: str, source_type: str) -> bool:
38+
"""Check whether a given signal source is enabled for a team.
39+
40+
LLM analytics signals are always allowed (gated in llma evals workflows). TODO - this should be moved here.
41+
For everything else, the team must have a SignalSourceConfig row with enabled=True.
42+
"""
43+
if source_product == cls.SourceProduct.LLM_ANALYTICS:
44+
return True
45+
46+
return cls.objects.filter(
47+
team_id=team_id,
48+
source_product=source_product,
49+
source_type=source_type,
50+
enabled=True,
51+
).exists()
52+
3253
class Meta:
3354
constraints = [
3455
models.UniqueConstraint(

products/signals/backend/test/test_api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import temporalio.exceptions
66

77
from products.signals.backend.api import emit_signal
8+
from products.signals.backend.models import SignalSourceConfig
89
from products.signals.backend.temporal.buffer import BufferSignalsWorkflow
910
from products.signals.backend.temporal.emitter import SignalEmitterWorkflow
1011

@@ -83,7 +84,10 @@ async def test_emit_signal_accepts_valid_input(self, source_product, source_type
8384
AsyncMock(), # emitter start
8485
]
8586

86-
with patch("products.signals.backend.api.async_connect", return_value=client):
87+
with (
88+
patch("products.signals.backend.api.async_connect", return_value=client),
89+
patch.object(SignalSourceConfig, "is_source_enabled", return_value=True),
90+
):
8791
await emit_signal(
8892
team=team_stub,
8993
source_product=source_product,
@@ -112,7 +116,10 @@ async def test_emit_signal_accepts_valid_input(self, source_product, source_type
112116
async def test_emit_signal_rejects_invalid_input(self, source_product, source_type, extra, team_stub):
113117
client = AsyncMock()
114118

115-
with patch("products.signals.backend.api.async_connect", return_value=client):
119+
with (
120+
patch("products.signals.backend.api.async_connect", return_value=client),
121+
patch.object(SignalSourceConfig, "is_source_enabled", return_value=True),
122+
):
116123
with pytest.raises(pydantic.ValidationError):
117124
await emit_signal(
118125
team=team_stub,

0 commit comments

Comments
 (0)