Skip to content

Commit d75e9ac

Browse files
committed
feat(investigations): add investigation schema
1 parent fbaadb1 commit d75e9ac

15 files changed

Lines changed: 1356 additions & 0 deletions

File tree

migrations_lockfile.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ hybridcloud: 0033_drop_webhookpayload_schedule_for_index
1919

2020
insights: 0001_squashed_0002_backfill_team_starred
2121

22+
investigations: 0001_initial
23+
2224
monitors: 0001_squashed_0013_delete_monitor_is_muted_field
2325

2426
nodestore: 0001_squashed_0002_nodestore_no_dictfield

src/sentry/conf/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ def env(
494494
"sentry.data_secrecy",
495495
"sentry.workflow_engine",
496496
"sentry.explore",
497+
"sentry.investigations.apps.InvestigationsConfig",
497498
"sentry.insights",
498499
"sentry.preprod",
499500
"sentry.releases",

src/sentry/investigations/__init__.py

Whitespace-only changes.

src/sentry/investigations/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class InvestigationsConfig(AppConfig):
5+
name = "sentry.investigations"

src/sentry/investigations/migrations/0001_initial.py

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.

src/sentry/investigations/migrations/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .cell import * # NOQA
2+
from .execution import * # NOQA
3+
from .investigation import * # NOQA
4+
from .parameter import * # NOQA
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from django.db import models
6+
from django.db.models import F, Q
7+
8+
from sentry.backup.scopes import RelocationScope
9+
from sentry.db.models import FlexibleForeignKey, cell_silo_model, sane_repr
10+
from sentry.db.models.base import DefaultFieldsModel
11+
from sentry.db.models.fields.bounded import BoundedPositiveIntegerField
12+
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
13+
14+
15+
class InvestigationCellKind(models.TextChoices):
16+
TEXT = "text", "Text"
17+
QUERY = "query", "Query"
18+
19+
20+
@cell_silo_model
21+
class InvestigationCell(DefaultFieldsModel):
22+
"""A user-composed cell whose content may be produced by a Seer execution."""
23+
24+
__relocation_scope__ = RelocationScope.Excluded
25+
26+
investigation = FlexibleForeignKey(
27+
"investigations.Investigation", on_delete=models.CASCADE, related_name="cells"
28+
)
29+
created_by_id = HybridCloudForeignKey("sentry.User", null=True, on_delete="SET_NULL")
30+
last_edited_by_id = HybridCloudForeignKey("sentry.User", null=True, on_delete="SET_NULL")
31+
32+
position = BoundedPositiveIntegerField()
33+
kind = models.CharField(max_length=32, choices=InvestigationCellKind.choices)
34+
title = models.CharField(max_length=255, default="", blank=True, db_default="")
35+
# The canonical, editable body rendered or evaluated by this cell.
36+
content = models.TextField(default="", blank=True, db_default="")
37+
# The latest prompt used to generate content. This remains editable so a
38+
# future execution can regenerate the cell.
39+
prompt = models.TextField(default="", blank=True, db_default="")
40+
# The unedited output of the latest generation. Human edits update content
41+
# without erasing the generated source.
42+
generated_content = models.TextField(default="", blank=True, db_default="")
43+
44+
# Kind-specific execution and authoring behavior, such as automatic execution
45+
# or a dataset hint. Presentation-only state belongs in `display`.
46+
config: models.Field[dict[str, Any], dict[str, Any]] = models.JSONField(
47+
default=dict, db_default={}
48+
)
49+
# Versioned presentation state, such as table/chart selection or whether a
50+
# prompt is collapsed. It must not affect how a cell executes.
51+
display: models.Field[dict[str, Any], dict[str, Any]] = models.JSONField(
52+
default=dict, db_default={}
53+
)
54+
55+
version = BoundedPositiveIntegerField(default=1, db_default=1)
56+
current_execution = FlexibleForeignKey(
57+
"investigations.InvestigationCellExecution",
58+
null=True,
59+
on_delete=models.SET_NULL,
60+
related_name="+",
61+
)
62+
# The successful execution that produced the currently rendered text body.
63+
# This remains stable while a newer generation is pending or fails so the
64+
# existing Markdown keeps its original project-access requirements.
65+
content_execution = FlexibleForeignKey(
66+
"investigations.InvestigationCellExecution",
67+
null=True,
68+
on_delete=models.SET_NULL,
69+
related_name="+",
70+
)
71+
# The latest successful execution that produced a query result. It remains
72+
# stable while a replacement run is pending, stopped, or fails.
73+
result_execution = FlexibleForeignKey(
74+
"investigations.InvestigationCellExecution",
75+
null=True,
76+
on_delete=models.SET_NULL,
77+
related_name="+",
78+
)
79+
# Set when an input changes so the UI can distinguish a valid old output
80+
# from a current one before a replacement execution finishes.
81+
stale_at = models.DateTimeField(null=True)
82+
83+
# Cells are hidden rather than hard-deleted so execution history remains
84+
# inspectable and stale references retain a stable target.
85+
deleted_at = models.DateTimeField(null=True)
86+
87+
class Meta:
88+
app_label = "investigations"
89+
db_table = "investigations_investigationcell"
90+
indexes = [
91+
models.Index(fields=["investigation", "deleted_at", "position"]),
92+
models.Index(fields=["investigation", "-date_updated"]),
93+
]
94+
95+
__repr__ = sane_repr("investigation_id", "kind", "position")
96+
97+
98+
@cell_silo_model
99+
class InvestigationCellDependency(DefaultFieldsModel):
100+
"""A directed edge from a cell to one of its upstream dependencies."""
101+
102+
__relocation_scope__ = RelocationScope.Excluded
103+
104+
cell = FlexibleForeignKey(
105+
"investigations.InvestigationCell",
106+
on_delete=models.CASCADE,
107+
related_name="dependency_links",
108+
)
109+
depends_on = FlexibleForeignKey(
110+
"investigations.InvestigationCell",
111+
on_delete=models.CASCADE,
112+
related_name="dependent_links",
113+
)
114+
115+
class Meta:
116+
app_label = "investigations"
117+
db_table = "investigations_investigationcelldependency"
118+
constraints = [
119+
models.UniqueConstraint(
120+
fields=["cell", "depends_on"], name="investigation_unique_cell_dependency"
121+
),
122+
models.CheckConstraint(
123+
condition=~Q(cell=F("depends_on")), name="investigation_no_self_dependency"
124+
),
125+
]
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from uuid import uuid4
5+
6+
from django.db import models
7+
8+
from sentry.backup.scopes import RelocationScope
9+
from sentry.db.models import FlexibleForeignKey, cell_silo_model, sane_repr
10+
from sentry.db.models.base import DefaultFieldsModel
11+
from sentry.db.models.fields.bounded import BoundedPositiveIntegerField
12+
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
13+
14+
15+
class InvestigationCellExecutor(models.TextChoices):
16+
# Content persisted without an automated runner.
17+
MANUAL = "manual", "Manual"
18+
# Content or query results produced by the Seer code-mode runner.
19+
CODE_MODE = "code_mode", "Code mode"
20+
# Text content produced by the Seer text-generation runner.
21+
TEXT_GENERATION = "text_generation", "Text generation"
22+
23+
24+
class InvestigationCellExecutionStatus(models.TextChoices):
25+
PENDING = "pending", "Pending"
26+
RUNNING = "running", "Running"
27+
AWAITING_INPUT = "awaiting_input", "Awaiting input"
28+
STOPPING = "stopping", "Stopping"
29+
COMPLETED = "completed", "Completed"
30+
FAILED = "failed", "Failed"
31+
CANCELLED = "cancelled", "Cancelled"
32+
33+
34+
@cell_silo_model
35+
class InvestigationCellExecution(DefaultFieldsModel):
36+
"""An immutable attempt to produce a cell's content or query result."""
37+
38+
__relocation_scope__ = RelocationScope.Excluded
39+
40+
# Idempotency identity supplied by, or returned to, execution callers.
41+
request_id = models.UUIDField(default=uuid4, editable=False, unique=True)
42+
cell = FlexibleForeignKey(
43+
"investigations.InvestigationCell", on_delete=models.CASCADE, related_name="executions"
44+
)
45+
triggered_by_id = HybridCloudForeignKey("sentry.User", null=True, on_delete="SET_NULL")
46+
seer_run = FlexibleForeignKey(
47+
"seer.SeerRun", null=True, on_delete=models.SET_NULL, related_name="cell_executions"
48+
)
49+
50+
executor = models.CharField(max_length=32, choices=InvestigationCellExecutor.choices)
51+
status = models.CharField(
52+
max_length=32,
53+
choices=InvestigationCellExecutionStatus.choices,
54+
default=InvestigationCellExecutionStatus.PENDING,
55+
db_default=InvestigationCellExecutionStatus.PENDING,
56+
)
57+
cell_version = BoundedPositiveIntegerField()
58+
59+
# Immutable resolved inputs: parameter values plus exact upstream execution
60+
# execution IDs/hashes. This makes reruns reproducible even after the notebook changes.
61+
input_snapshot: models.Field[dict[str, Any], dict[str, Any]] = models.JSONField(
62+
default=dict, db_default={}
63+
)
64+
input_fingerprint = models.CharField(max_length=64)
65+
66+
result_schema_version = BoundedPositiveIntegerField(default=1, db_default=1)
67+
result = models.JSONField(null=True)
68+
error = models.JSONField(null=True)
69+
transcript = models.JSONField(default=list, db_default=[])
70+
transcript_truncated = models.BooleanField(default=False, db_default=False)
71+
started_at = models.DateTimeField(null=True)
72+
completed_at = models.DateTimeField(null=True)
73+
74+
# Immutable provenance for projects whose data contributed to this output.
75+
# Unlike an investigation's mutable project selection, these links are used
76+
# when enforcing access to already-persisted results.
77+
data_projects = models.ManyToManyField(
78+
"sentry.Project", through="investigations.InvestigationCellExecutionProject", blank=True
79+
)
80+
81+
class Meta:
82+
app_label = "investigations"
83+
db_table = "investigations_investigationcellexecution"
84+
indexes = [
85+
models.Index(fields=["cell", "-date_added"]),
86+
models.Index(fields=["cell", "status"]),
87+
]
88+
89+
__repr__ = sane_repr("cell_id", "executor", "status")
90+
91+
92+
@cell_silo_model
93+
class InvestigationCellExecutionProject(DefaultFieldsModel):
94+
"""A project whose data contributed to one persisted cell output."""
95+
96+
__relocation_scope__ = RelocationScope.Excluded
97+
98+
execution = FlexibleForeignKey(
99+
"investigations.InvestigationCellExecution",
100+
on_delete=models.CASCADE,
101+
related_name="data_project_links",
102+
)
103+
project = FlexibleForeignKey("sentry.Project", on_delete=models.CASCADE)
104+
105+
class Meta:
106+
app_label = "investigations"
107+
db_table = "investigations_investigationcellexecutionproject"
108+
constraints = [
109+
models.UniqueConstraint(
110+
fields=["execution", "project"],
111+
name="investigation_unique_execution_project",
112+
)
113+
]

0 commit comments

Comments
 (0)