Skip to content

feat(scheduled reports): Create ScheduledReport model with migration & feature flag#108386

Open
shashjar wants to merge 5 commits intomasterfrom
shashjar/scheduled-reports-data-model-migration-feature-flag
Open

feat(scheduled reports): Create ScheduledReport model with migration & feature flag#108386
shashjar wants to merge 5 commits intomasterfrom
shashjar/scheduled-reports-data-model-migration-feature-flag

Conversation

@shashjar
Copy link
Member

@shashjar shashjar commented Feb 17, 2026

https://www.notion.so/sentry/Scheduled-Reports-on-Saved-Queries-Dashboards-3098b10e4b5d8017808bdea381cb4465

PR 1 for adding scheduled report delivery for Explore saved queries (CSV email attachments) and eventually Dashboards (likely PDF email attachments).

  • Creates ScheduledReport model in src/sentry/reports/models.py with scheduling configuration (frequency, day/time, etc.), polymorphic source reference (Explore saved queries and Dashboards), recipient email addresses, and scheduling state.
  • Adds the migration 1031_create_scheduled_report_model to create the above model.
  • Registers the organizations:scheduled-reports feature flag to gate this feature.

…te feature flag for gating scheduled reports
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Feb 17, 2026
@github-actions
Copy link
Contributor

This PR has a migration; here is the generated SQL for src/sentry/migrations/1031_create_scheduled_report_model.py

for 1031_create_scheduled_report_model in sentry

--
-- Create model ScheduledReport
--
CREATE TABLE "sentry_scheduledreport" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "created_by_id" bigint NULL, "name" varchar(255) NOT NULL, "source_type" integer NOT NULL CHECK ("source_type" >= 0), "source_id" bigint NOT NULL, "frequency" integer NOT NULL CHECK ("frequency" >= 0), "day_of_week" integer NULL CHECK ("day_of_week" >= 0), "day_of_month" integer NULL CHECK ("day_of_month" >= 0), "hour" integer NOT NULL CHECK ("hour" >= 0), "time_range" varchar(32) NULL, "recipient_emails" jsonb NOT NULL, "is_active" boolean NOT NULL, "next_run_at" timestamp with time zone NOT NULL, "organization_id" bigint NOT NULL, CONSTRAINT "sentry_scheduledreport_valid_time_range" CHECK (("time_range" IN ('1h', '1d', '24h', '7d', '14d', '30d', '90d') OR "time_range" IS NULL)));
ALTER TABLE "sentry_scheduledreport" ADD CONSTRAINT "sentry_scheduledrepo_organization_id_17c1a16b_fk_sentry_or" FOREIGN KEY ("organization_id") REFERENCES "sentry_organization" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "sentry_scheduledreport" VALIDATE CONSTRAINT "sentry_scheduledrepo_organization_id_17c1a16b_fk_sentry_or";
CREATE INDEX CONCURRENTLY "sentry_scheduledreport_created_by_id_583538e5" ON "sentry_scheduledreport" ("created_by_id");
CREATE INDEX CONCURRENTLY "sentry_scheduledreport_source_id_c9a6d438" ON "sentry_scheduledreport" ("source_id");
CREATE INDEX CONCURRENTLY "sentry_scheduledreport_next_run_at_7da9d749" ON "sentry_scheduledreport" ("next_run_at");
CREATE INDEX CONCURRENTLY "sentry_scheduledreport_organization_id_17c1a16b" ON "sentry_scheduledreport" ("organization_id");
CREATE INDEX CONCURRENTLY "sentry_sche_organiz_15bc2b_idx" ON "sentry_scheduledreport" ("organization_id", "source_type", "source_id");
CREATE INDEX CONCURRENTLY "sentry_sche_is_acti_fbe604_idx" ON "sentry_scheduledreport" ("is_active", "next_run_at");

@shashjar shashjar requested review from a team February 18, 2026 01:48
@shashjar shashjar marked this pull request as ready for review February 18, 2026 01:50
@shashjar shashjar requested a review from a team as a code owner February 18, 2026 01:50
@shashjar shashjar removed the request for review from a team February 18, 2026 01:51
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

next_run_at = models.DateTimeField(db_index=True)

class Meta:
app_label = "sentry"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this part of reports so that it gets its own separate migration file

Comment on lines +46 to +50
# Polymorphic source reference: points to ExploreSavedQuery.id or Dashboard.id
source_type = BoundedPositiveIntegerField(
choices=ScheduledReportSourceType.as_choices(),
)
source_id = BoundedBigIntegerField(db_index=True)
Copy link
Member

@wedamija wedamija Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other option here could be to include the FKs to both, make them nullable and have a constraint to allow only one. Not sure if you're adding more types over time though.

The main benefit there is that if the linked row is deleted, then this row will be removed, rather than being orphaned. Up to you to decide what's best though

Comment on lines +52 to +58
# Schedule configuration
frequency = BoundedPositiveIntegerField(
choices=ScheduledReportFrequency.as_choices(),
)
day_of_week = BoundedPositiveIntegerField(null=True, blank=True) # 0=Monday, for weekly
day_of_month = BoundedPositiveIntegerField(null=True, blank=True) # 1-31, for monthly
hour = BoundedPositiveIntegerField() # 0-23, stored in UTC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could considering looking at how cron monitors handles out schedules. I'm wondering if this makes sense as just a crontab string?

Comment on lines +65 to +66
# Delivery configuration: list of org member email strings
recipient_emails = models.JSONField(default=list)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to link to the org member rows explicitly. That way, when an org member is removed, we stop sending these reports to them automatically, rather than having to remember to clean up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants