Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .semgrep/rules/security/idor-team-scoped-models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ rules:
|ScoreDefinition
|SavedHeatmap
|HeatmapCaptureConfigVersion
|HeatmapCaptureRequest
|HeatmapScreenshotHistory
|ScheduledChange
|SessionGroupSummary
|SessionRecording
Expand Down Expand Up @@ -797,6 +799,8 @@ rules:
|ScoreDefinition
|SavedHeatmap
|HeatmapCaptureConfigVersion
|HeatmapCaptureRequest
|HeatmapScreenshotHistory
|ScheduledChange
|SessionGroupSummary
|SessionRecording
Expand Down
185 changes: 185 additions & 0 deletions products/web_analytics/backend/migrations/0015_heatmap_page_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Generated by Django 5.2.18 on 2026-10-08 11:07

import django.utils.timezone
import django.db.models.deletion
from django.db import migrations, models

import posthog.uuidt


class Migration(migrations.Migration):
dependencies = [
("posthog", "1394_backfill_secret_tokens_to_psak"),
("web_analytics", "0014_content_autopilot_opportunities"),
]
operations = [
migrations.CreateModel(
name="HeatmapScreenshotHistory",
fields=[
(
"id",
models.UUIDField(default=posthog.uuidt.uuid7, editable=False, primary_key=True, serialize=False),
),
("width", models.IntegerField()),
("captured_on", models.DateField()),
("captured_at", models.DateTimeField(blank=True, null=True)),
("timezone", models.CharField(max_length=100)),
("day_start", models.DateTimeField()),
("day_end", models.DateTimeField()),
("expires_at", models.DateTimeField(db_index=True)),
("revision", models.UUIDField(blank=True, null=True)),
("has_thumbnail", models.BooleanField(default=False)),
(
"trigger",
models.CharField(
choices=[("scheduled", "Daily capture"), ("manual", "Requested capture")],
default="scheduled",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[("ok", "Captured"), ("pending", "Capture pending"), ("failed", "Capture failed")],
max_length=20,
),
),
("failure_cause", models.CharField(blank=True, max_length=100, null=True)),
("page_status", models.IntegerField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"heatmap",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="screenshot_history",
to="web_analytics.savedheatmap",
),
),
(
"team",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="posthog.team",
),
),
],
options={
"db_table": "posthog_heatmapscreenshothistory",
"constraints": [
models.UniqueConstraint(fields=("heatmap", "captured_on"), name="heatmap_history_one_per_day")
],
},
),
migrations.CreateModel(
name="HeatmapCaptureRequest",
fields=[
(
"id",
models.UUIDField(default=posthog.uuidt.uuid7, editable=False, primary_key=True, serialize=False),
),
(
"state",
models.CharField(
choices=[
("queued", "Queued"),
("running", "Running"),
("succeeded", "Succeeded"),
("failed", "Failed"),
("cancelled", "Cancelled"),
],
default="queued",
max_length=16,
),
),
(
"trigger",
models.CharField(
choices=[("scheduled", "Daily capture"), ("manual", "Requested capture")], max_length=20
),
),
("captured_on", models.DateField()),
("timezone", models.CharField(max_length=100)),
("day_start", models.DateTimeField()),
("day_end", models.DateTimeField()),
("width", models.PositiveIntegerField()),
("url", models.URLField(max_length=2000)),
("block_consent_modals", models.BooleanField()),
("input_signature", models.CharField(max_length=64)),
("claim_id", models.UUIDField(null=True)),
("deadline", models.DateTimeField()),
("expires_at", models.DateTimeField(db_index=True)),
("failure_cause", models.CharField(max_length=100, null=True)),
("page_status", models.IntegerField(null=True)),
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
("completed_at", models.DateTimeField(null=True)),
(
"heatmap",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="history_requests",
to="web_analytics.savedheatmap",
),
),
(
"team",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="posthog.team",
),
),
(
"history",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="requests",
to="web_analytics.heatmapscreenshothistory",
),
),
],
options={
"db_table": "posthog_heatmapcapturerequest",
"indexes": [
models.Index(fields=["trigger", "created_at"], name="heatmap_request_global_idx"),
models.Index(fields=["team", "captured_on", "trigger"], name="heatmap_request_team_idx"),
models.Index(
condition=models.Q(("state__in", ["queued", "running"])),
fields=["deadline"],
name="heatmap_request_live_idx",
),
],
"constraints": [
models.UniqueConstraint(
condition=models.Q(("trigger", "scheduled")),
fields=("heatmap", "captured_on"),
name="heatmap_history_one_scheduled",
)
],
},
),
migrations.AddField(
model_name="savedheatmap", name="last_viewed_at", field=models.DateTimeField(blank=True, null=True)
),
migrations.AddField(
model_name="savedheatmap", name="next_history_capture_at", field=models.DateTimeField(blank=True, null=True)
),
migrations.AddField(
model_name="savedheatmap",
name="history_configuration_revision",
field=models.UUIDField(blank=True, null=True),
),
migrations.AddField(
model_name="heatmapscreenshothistory",
name="latest_request",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="web_analytics.heatmapcapturerequest",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models

from posthog.migration_helpers import SafeAddIndexConcurrently


class Migration(migrations.Migration):
atomic = False

dependencies = [("web_analytics", "0015_heatmap_page_history")]

operations = [
SafeAddIndexConcurrently(
model_name="savedheatmap",
index=models.Index(fields=["last_viewed_at"], name="heatmap_last_viewed_idx"),
),
]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0014_content_autopilot_opportunities
0016_savedheatmap_last_viewed_idx
3 changes: 3 additions & 0 deletions products/web_analytics/backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
)
from .heatmap_capture_config_version import HeatmapCaptureConfigVersion
from .heatmap_saved import HeatmapSnapshot, SavedHeatmap
from .heatmap_screenshot_history import HeatmapCaptureRequest, HeatmapScreenshotHistory
from .web_analytics_achievement_progress import WebAnalyticsAchievementProgress
from .web_analytics_filter_preset import WebAnalyticsFilterPreset
from .web_analytics_interaction import WebAnalyticsInteraction
from .web_analytics_user_config import WebAnalyticsUserConfig
from .web_analytics_visit import WebAnalyticsVisit

__all__ = [
"HeatmapCaptureRequest",
"HeatmapCaptureConfigVersion",
"HeatmapScreenshotHistory",
"HeatmapSnapshot",
"SavedHeatmap",
"ContentAutopilotOpportunity",
Expand Down
4 changes: 4 additions & 0 deletions products/web_analytics/backend/models/heatmap_saved.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class Source(models.TextChoices):
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_viewed_at = models.DateTimeField(null=True, blank=True)
history_configuration_revision = models.UUIDField(null=True, blank=True)
next_history_capture_at = models.DateTimeField(null=True, blank=True)
created_by = models.ForeignKey("posthog.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="+")

# Error handling
Expand All @@ -51,6 +54,7 @@ class Meta:
models.Index(fields=["team", "url"]),
models.Index(fields=["status"]),
models.Index(fields=["deleted"]),
models.Index(fields=["last_viewed_at"], name="heatmap_last_viewed_idx"),
]
constraints = []
unique_together = ("team", "short_id")
Expand Down
104 changes: 104 additions & 0 deletions products/web_analytics/backend/models/heatmap_screenshot_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from django.db import models
from django.utils.timezone import now

from posthog.models.scoping.root_mixin import TeamScopedRootMixin
from posthog.models.utils import UUIDModel

from products.web_analytics.backend.models.heatmap_saved import SavedHeatmap


class HeatmapScreenshotHistory(TeamScopedRootMixin, UUIDModel):
class Status(models.TextChoices):
OK = "ok", "Captured"
PENDING = "pending", "Capture pending"
FAILED = "failed", "Capture failed"

class Trigger(models.TextChoices):
SCHEDULED = "scheduled", "Daily capture"
MANUAL = "manual", "Requested capture"

class ImageVariant(models.TextChoices):
FULL = "full", "Full page"
THUMBNAIL = "thumbnail", "Thumbnail"

team = models.ForeignKey("posthog.Team", on_delete=models.CASCADE, db_constraint=False, related_name="+")
heatmap = models.ForeignKey(SavedHeatmap, on_delete=models.CASCADE, related_name="screenshot_history")
width = models.IntegerField()
captured_on = models.DateField()
captured_at = models.DateTimeField(null=True, blank=True)
timezone = models.CharField(max_length=100)
day_start = models.DateTimeField()
day_end = models.DateTimeField()
expires_at = models.DateTimeField(db_index=True)
revision = models.UUIDField(null=True, blank=True)
has_thumbnail = models.BooleanField(default=False)
trigger = models.CharField(max_length=20, choices=Trigger, default=Trigger.SCHEDULED)
status = models.CharField(max_length=20, choices=Status)
failure_cause = models.CharField(max_length=100, null=True, blank=True)
page_status = models.IntegerField(null=True, blank=True)
latest_request = models.ForeignKey(
"web_analytics.HeatmapCaptureRequest", on_delete=models.SET_NULL, null=True, related_name="+"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
db_table = "posthog_heatmapscreenshothistory"
constraints = [
models.UniqueConstraint(fields=["heatmap", "captured_on"], name="heatmap_history_one_per_day"),
]

@property
def has_content(self) -> bool:
return self.revision is not None


class HeatmapCaptureRequest(TeamScopedRootMixin, UUIDModel):
class State(models.TextChoices):
QUEUED = "queued", "Queued"
RUNNING = "running", "Running"
SUCCEEDED = "succeeded", "Succeeded"
FAILED = "failed", "Failed"
CANCELLED = "cancelled", "Cancelled"

LIVE_STATES = (State.QUEUED, State.RUNNING)

team = models.ForeignKey("posthog.Team", on_delete=models.CASCADE, db_constraint=False, related_name="+")
heatmap = models.ForeignKey(SavedHeatmap, on_delete=models.CASCADE, related_name="history_requests")
history = models.ForeignKey(HeatmapScreenshotHistory, on_delete=models.CASCADE, related_name="requests")
state = models.CharField(max_length=16, choices=State, default=State.QUEUED)
trigger = models.CharField(max_length=20, choices=HeatmapScreenshotHistory.Trigger)
captured_on = models.DateField()
timezone = models.CharField(max_length=100)
day_start = models.DateTimeField()
day_end = models.DateTimeField()
width = models.PositiveIntegerField()
url = models.URLField(max_length=2000)
block_consent_modals = models.BooleanField()
input_signature = models.CharField(max_length=64)
claim_id = models.UUIDField(null=True)
deadline = models.DateTimeField()
expires_at = models.DateTimeField(db_index=True)
failure_cause = models.CharField(max_length=100, null=True)
page_status = models.IntegerField(null=True)
created_at = models.DateTimeField(default=now)
completed_at = models.DateTimeField(null=True)

class Meta:
db_table = "posthog_heatmapcapturerequest"
constraints = [
models.UniqueConstraint(
fields=["heatmap", "captured_on"],
condition=models.Q(trigger="scheduled"),
name="heatmap_history_one_scheduled",
),
]
indexes = [
models.Index(fields=["trigger", "created_at"], name="heatmap_request_global_idx"),
models.Index(fields=["team", "captured_on", "trigger"], name="heatmap_request_team_idx"),
models.Index(
fields=["deadline"],
condition=models.Q(state__in=["queued", "running"]),
name="heatmap_request_live_idx",
),
]
Loading