Browse Source

feat(replay): Updated weekly email to optionally include Total Project Replays section (#51388)

Relates to https://github.com/getsentry/sentry/issues/51369

Sharing early with some debugging code still in here, and unimplemented
spots too

You can see example emails by running `sentry devserver` and visiting
http://dev.getsentry.net:8000/debug/mail/weekly-reports/

There are 6 ui cases that can happen

Note that the 'setup replays' copy in the table below is not final.
These screens are to test padding/borders and layout.

The real copy is looking like (still need the Play image added):

![SCR-20230622-odyf](https://github.com/getsentry/sentry/assets/187460/a5eb1f36-8153-4a3e-a094-98e910ea5fb8)


| total_transaction_count | has_replay_section #L742 |
total_replay_count | | layout | txn view? | replay view? | img |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | False | n/a | | two-up | setup | n/a | ![setup
txn](https://github.com/getsentry/sentry/assets/187460/b2e024e6-e07f-4099-81ac-36cfb27f927f)
|
| 0 | True | 0 | | three-up | setup | setup | ![setup txn - setup
replay](https://github.com/getsentry/sentry/assets/187460/7f677185-1332-4ee0-82d5-6d3c022ac963)
| 0 | True | >=1| | three-up | setup | graph | ![setup txn - replay
graph](https://github.com/getsentry/sentry/assets/187460/4c07e80d-a2af-4248-acbc-c69c7ffabb28)
|
| >=1 | False | n/a | | two-up | graph | n/a | ![txn
data](https://github.com/getsentry/sentry/assets/187460/3ce99426-4aa8-4a85-b94b-fb4e2db3ed8d)
|
| >=1 | True | 0 | | three-up | graph | setup | ![txn data - setup
replay](https://github.com/getsentry/sentry/assets/187460/4b80c404-38bb-4bf4-96db-9cf0323dfe8a)
|
| >=1 | True | >=1 | | three-up | graph | graph | ![txn data - replay
data](https://github.com/getsentry/sentry/assets/187460/1f1a8cf9-5a11-4a6b-91c8-2b967d569800)
|

---------

Co-authored-by: Joshua Ferge <josh.ferge@sentry.io>
Ryan Albrecht 1 year ago
parent
commit
8ba41ef70b

+ 1 - 0
src/sentry/conf/server.py

@@ -1559,6 +1559,7 @@ SENTRY_FEATURES = {
     "organizations:session-replay-recording-scrubbing": False,
     # Enable subquery optimizations for the replay_index page
     "organizations:session-replay-index-subquery": False,
+    "organizations:session-replay-weekly-email": False,
     # Enable the new suggested assignees feature
     "organizations:streamline-targeting-context": False,
     # Enable the new experimental starfish view

+ 1 - 0
src/sentry/features/__init__.py

@@ -178,6 +178,7 @@ default_manager.add("organizations:session-replay-sdk", OrganizationFeature, Fea
 default_manager.add("organizations:session-replay-sdk-errors-only", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
 default_manager.add("organizations:session-replay-recording-scrubbing", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:session-replay-index-subquery", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
+default_manager.add("organizations:session-replay-weekly-email", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:set-grouping-config", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("organizations:slack-overage-notifications", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
 default_manager.add("organizations:sdk-crash-detection", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)

BIN
src/sentry/static/sentry/images/email/icon-circle-play.png


+ 62 - 4
src/sentry/tasks/weekly_reports.py

@@ -73,6 +73,8 @@ class ProjectContext:
     dropped_error_count = 0
     accepted_transaction_count = 0
     dropped_transaction_count = 0
+    accepted_replay_count = 0
+    dropped_replay_count = 0
 
     # Removed after organizations:escalating-issues GA
     all_issue_count = 0
@@ -97,13 +99,24 @@ class ProjectContext:
         # Array of (Group, count)
         self.key_performance_issues = []
 
+        self.key_replay_events = []
+
         # Dictionary of { timestamp: count }
         self.error_count_by_day = {}
         # Dictionary of { timestamp: count }
         self.transaction_count_by_day = {}
+        # Dictionary of { timestamp: count }
+        self.replay_count_by_day = {}
 
     def __repr__(self):
-        return f"{self.key_errors}, Errors: [Accepted {self.accepted_error_count}, Dropped {self.dropped_error_count}]\nTransactions: [Accepted {self.accepted_transaction_count} Dropped {self.dropped_transaction_count}]"
+        return "\n".join(
+            [
+                f"{self.key_errors}, ",
+                f"Errors: [Accepted {self.accepted_error_count}, Dropped {self.dropped_error_count}]",
+                f"Transactions: [Accepted {self.accepted_transaction_count} Dropped {self.dropped_transaction_count}]",
+                f"Replays: [Accepted {self.accepted_replay_count} Dropped {self.dropped_replay_count}]",
+            ]
+        )
 
 
 def check_if_project_is_empty(project_ctx):
@@ -118,6 +131,8 @@ def check_if_project_is_empty(project_ctx):
         and not project_ctx.dropped_error_count
         and not project_ctx.accepted_transaction_count
         and not project_ctx.dropped_transaction_count
+        and not project_ctx.accepted_replay_count
+        and not project_ctx.dropped_replay_count
     )
 
 
@@ -246,7 +261,7 @@ def project_event_counts_for_organization(ctx):
             Condition(
                 Column("category"),
                 Op.IN,
-                [*DataCategory.error_categories(), DataCategory.TRANSACTION],
+                [*DataCategory.error_categories(), DataCategory.TRANSACTION, DataCategory.REPLAY],
             ),
         ],
         groupby=[Column("outcome"), Column("category"), Column("project_id"), Column("time")],
@@ -268,6 +283,13 @@ def project_event_counts_for_organization(ctx):
             else:
                 project_ctx.accepted_transaction_count += total
                 project_ctx.transaction_count_by_day[timestamp] = total
+        elif dat["category"] == DataCategory.REPLAY:
+            # Replay outcome
+            if dat["outcome"] == Outcome.RATE_LIMITED or dat["outcome"] == Outcome.FILTERED:
+                project_ctx.dropped_replay_count += total
+            else:
+                project_ctx.accepted_replay_count += total
+                project_ctx.replay_count_by_day[timestamp] = total
         else:
             # Error outcome
             if dat["outcome"] == Outcome.RATE_LIMITED or dat["outcome"] == Outcome.FILTERED:
@@ -702,6 +724,9 @@ def render_template_context(ctx, user):
         user_projects = ctx.projects.values()
 
     has_issue_states = features.has("organizations:escalating-issues", ctx.organization)
+    has_replay_section = features.has(
+        "organizations:session-replay", ctx.organization
+    ) and features.has("organizations:session-replay-weekly-email", ctx.organization)
 
     # Render the first section of the email where we had the table showing the
     # number of accepted/dropped errors/transactions for each project.
@@ -709,17 +734,26 @@ def render_template_context(ctx, user):
         # Given an iterator of event counts, sum up their accepted/dropped errors/transaction counts.
         def sum_event_counts(project_ctxs):
             return reduce(
-                lambda a, b: (a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]),
+                lambda a, b: (
+                    a[0] + b[0],
+                    a[1] + b[1],
+                    a[2] + b[2],
+                    a[3] + b[3],
+                    a[4] + b[4],
+                    a[5] + b[5],
+                ),
                 [
                     (
                         project_ctx.accepted_error_count,
                         project_ctx.dropped_error_count,
                         project_ctx.accepted_transaction_count,
                         project_ctx.dropped_transaction_count,
+                        project_ctx.accepted_replay_count,
+                        project_ctx.dropped_replay_count,
                     )
                     for project_ctx in project_ctxs
                 ],
-                (0, 0, 0, 0),
+                (0, 0, 0, 0, 0, 0),
             )
 
         # Highest volume projects go first
@@ -734,7 +768,10 @@ def render_template_context(ctx, user):
             total_dropped_error,
             total_transaction,
             total_dropped_transaction,
+            total_replays,
+            total_dropped_replays,
         ) = sum_event_counts(projects_associated_with_user)
+
         # The number of reports to keep is the same as the number of colors
         # available to use in the legend.
         projects_taken = projects_associated_with_user[: len(project_breakdown_colors)]
@@ -751,6 +788,8 @@ def render_template_context(ctx, user):
                 "accepted_error_count": project_ctx.accepted_error_count,
                 "dropped_transaction_count": project_ctx.dropped_transaction_count,
                 "accepted_transaction_count": project_ctx.accepted_transaction_count,
+                "dropped_replay_count": project_ctx.dropped_replay_count,
+                "accepted_replay_count": project_ctx.accepted_replay_count,
             }
             for i, project_ctx in enumerate(projects_taken)
         ]
@@ -761,6 +800,8 @@ def render_template_context(ctx, user):
                 others_dropped_error,
                 others_transaction,
                 others_dropped_transaction,
+                others_replays,
+                others_dropped_replays,
             ) = sum_event_counts(projects_not_taken)
             legend.append(
                 {
@@ -770,6 +811,8 @@ def render_template_context(ctx, user):
                     "accepted_error_count": others_error,
                     "dropped_transaction_count": others_dropped_transaction,
                     "accepted_transaction_count": others_transaction,
+                    "dropped_replay_count": others_dropped_replays,
+                    "accepted_replay_count": others_replays,
                 }
             )
         if len(projects_taken) > 1:
@@ -781,6 +824,8 @@ def render_template_context(ctx, user):
                     "accepted_error_count": total_error,
                     "dropped_transaction_count": total_dropped_transaction,
                     "accepted_transaction_count": total_transaction,
+                    "dropped_replay_count": total_dropped_replays,
+                    "accepted_replay_count": total_replays,
                 }
             )
 
@@ -793,6 +838,7 @@ def render_template_context(ctx, user):
                     "color": project_breakdown_colors[i],
                     "error_count": project_ctx.error_count_by_day.get(t, 0),
                     "transaction_count": project_ctx.transaction_count_by_day.get(t, 0),
+                    "replay_count": project_ctx.replay_count_by_day.get(t, 0),
                 }
                 for i, project_ctx in enumerate(projects_taken)
             ]
@@ -812,6 +858,12 @@ def render_template_context(ctx, user):
                                 projects_not_taken,
                             )
                         ),
+                        "replay_count": sum(
+                            map(
+                                lambda project_ctx: project_ctx.replay_count_by_day.get(t, 0),
+                                projects_not_taken,
+                            )
+                        ),
                     }
                 )
             series.append((to_datetime(t), project_series))
@@ -820,11 +872,15 @@ def render_template_context(ctx, user):
             "series": series,
             "total_error_count": total_error,
             "total_transaction_count": total_transaction,
+            "total_replay_count": total_replays,
             "error_maximum": max(  # The max error count on any single day
                 sum(value["error_count"] for value in values) for timestamp, values in series
             ),
             "transaction_maximum": max(  # The max transaction count on any single day
                 sum(value["transaction_count"] for value in values) for timestamp, values in series
+            ),
+            "replay_maximum": max(  # The max replay count on any single day
+                sum(value["replay_count"] for value in values) for timestamp, values in series
             )
             if len(projects_taken) > 0
             else 0,
@@ -951,6 +1007,7 @@ def render_template_context(ctx, user):
         }
 
     return {
+        "has_replay_section": has_replay_section,
         "organization": ctx.organization,
         "start": date_format(ctx.start),
         "end": date_format(ctx.end),
@@ -958,6 +1015,7 @@ def render_template_context(ctx, user):
         "key_errors": key_errors(),
         "key_transactions": key_transactions(),
         "key_performance_issues": key_performance_issues(),
+        "key_replays": [],
         "issue_summary": issue_summary(),
     }
 

+ 109 - 3
src/sentry/templates/sentry/emails/reports/body.html

@@ -77,13 +77,42 @@
     .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.errors {
       padding-right: 10px;
     }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.errors-wide {
+      padding-bottom: 10px;
+    }
     .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.transactions {
       padding-left: 10px;
       border: 1px solid #C4C4C4;
     }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.transactions-below {
+      padding-top: 10px;
+      padding-left: 10px;
+      padding-right: 10px;
+      border: 1px solid #C4C4C4;
+    }
     .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.transactions-empty {
       padding-left: 10px;
     }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.transactions-empty-below {
+      padding-top: 10px;
+      padding-left: 10px;
+      padding-right: 10px;
+      border-top: 1px solid #C4C4C4;
+    }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.replays {
+      padding-top: 10px;
+      padding-left: 10px;
+      border: 1px solid #C4C4C4;
+    }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.replays-empty {
+      padding-top: 10px;
+      padding-left: 10px;
+      border-top: 1px solid #C4C4C4;
+    }
+    .project-breakdown table.project-breakdown-graph-deck td.project-breakdown-graph-cell.some-empty-below {
+      border-left: none;
+      border-right: none;
+    }
     .project-breakdown .total-count-title {
       margin-top: 0;
       margin-bottom: 0;
@@ -200,7 +229,7 @@
     {% with height=110 %}
     <table class="project-breakdown-graph-deck"><tbody><tr>
 
-    <td class="project-breakdown-graph-cell errors">
+    <td class="project-breakdown-graph-cell {% if has_replay_section %}errors-wide{% else %}errors{% endif %}" {% if has_replay_section %}colspan="2"{% endif %}>
     <h4 class="total-count-title">Total Project Errors</h4>
     <h1 style="margin: 0;" class="total-count">{{ trends.total_error_count|small_count:1 }}</h1>
     {% url 'sentry-organization-issue-list' organization.slug as issue_list %}
@@ -235,8 +264,13 @@
     </table>
     </td>
 
+{% if has_replay_section %}
+</tr>
+<tr>
+{% endif %}
+
     {% if trends.total_transaction_count > 0 %}
-    <td class="project-breakdown-graph-cell transactions">
+    <td class="project-breakdown-graph-cell {% if has_replay_section %}transactions-below{% else %}transactions{% endif %} {% if has_replay_section and trends.total_replay_count == 0 %}some-empty-below{% endif %}">
       <h4 class="total-count-title">Total Project Transactions</h4>
       <h1 style="margin: 0;" class="total-count">{{ trends.total_transaction_count|small_count:1 }}</h1>
       {% url 'sentry-organization-perfomance' organization.slug as performance_landing %}
@@ -270,7 +304,7 @@
       </table>
       </td>
     {% else %}
-      <td class="project-breakdown-graph-cell transactions-empty">
+      <td class="project-breakdown-graph-cell {% if has_replay_section and trends.total_replay_count > 0 %}transactions-empty-below{% elif has_replay_section %}transactions-empty-below some-empty-below{% else %}transactions-empty{% endif %}">
         <div style="border: 1px solid #c4c4cc; border-radius: 4px; padding: 24px 16px; text-align: center; height: 170px;">
           <img src="{% absolute_asset_url 'sentry' 'images/email/icon-circle-lightning.png' %}" width="32px" height="32px" alt="Sentry">
           <h1 style="font-weight: bold; font-size: 17px;">Something slow?</h1>
@@ -282,6 +316,56 @@
       </td>
     {% endif %}
 
+    {% if has_replay_section and trends.total_replay_count > 0 %}
+      <td class="project-breakdown-graph-cell replays {% if trends.total_transaction_count == 0 %}some-empty-below{% endif %}">
+        <h4 class="total-count-title">Total Project Replays</h4>
+        <h1 style="margin: 0;" class="total-count">{{ trends.total_replay_count|small_count:1 }}</h1>
+        {% url 'sentry-organization-replays' organization.slug as replay_landing %}
+        <a href="{% org_url organization replay_landing query='referrer=weekly_email_view_all' %}"
+          style="font-size: 12px; margin-bottom: 16px; display: block;">View All Replays</a>
+        <table class="graph">
+          <tr>
+            {% for timestamp, project_values in trends.series %}
+            <td valign="bottom" class="bar"
+              style="height: {{ height }}px; width: {% widthratio 1 trends.series|length 100 %}%">
+              <table class="bar">
+                {% for project_value in project_values %}
+                <tr>
+                  <td height="{% widthratio project_value.replay_count trends.replay_maximum height %}"
+                    style="background-color: {{ project_value.color }};">&nbsp;</td>
+                </tr>
+                {% empty %}
+                <tr>
+                  <td height="1" style="background-color: #ebe9f7;"></td>
+                </tr>
+                {% endfor %}
+              </table>
+            </td>
+            {% endfor %}
+          </tr>
+          <tr>
+            {% for timestamp, project_values in trends.series %}
+            <td class="label" style="width: {% widthratio 1 trends.series|length 100 %}%">
+              {{ timestamp|date:"D" }}
+            </td>
+            {% endfor %}
+          </tr>
+        </table>
+      </td>
+    {% elif has_replay_section %}
+      <td class="project-breakdown-graph-cell {% if trends.total_transaction_count > 0 %}replays-empty some-empty-below{% else %}replays-empty{% endif %}">
+        <div style="border: 1px solid #c4c4cc; border-radius: 4px; padding: 24px 16px; text-align: center; height: 170px;">
+          <img src="{% absolute_asset_url 'sentry' 'images/email/icon-circle-play.png' %}" width="32px" height="32px"
+            alt="Sentry">
+          <h1 style="font-weight: bold; font-size: 17px;">Tricky bug?</h1>
+          <p style="font-size: 11px;">Rewind and replay every step of a user’s journey before and after they encounter an issue.</p>
+          {% url 'sentry-organization-replays' organization.slug as replay_landing %}
+          <a href="{% org_url organization replay_landing query='referrer=weekly_email_upsell' %}" class="btn"
+            style="margin-top: 8px;">Set Up Session Replay</a>
+        </div>
+      </td>
+    {% endif %}
+
     </tr></tbody></table>
 
     <table class="summary">
@@ -449,5 +533,27 @@
     {% endfor %}
   </div>
   {% endif %}
+
+  {% if has_replay_section and key_replays|length > 0 %}
+  <div id="key-replays">
+    <h4>Most erroneous replays</h4>
+    {% for a in key_replays %}
+    <div style="display: flex; flex-direction: row; margin-bottom: 8px; align-items: flex-start;">
+      <div style="width: 10%; font-size: 17px;">{{a.count|small_count:1}}</div>
+      <div style="width: 65%;">
+        {% querystring referrer="weekly_report" as query %}
+        {% url 'sentry-organization-replay-details' organization.slug a.replay.id as replay_details %}
+        <a style="display: block; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; font-size: 17px;"
+          href="{% org_url organization replay_details query=query %}">{{a.id}}</a>
+        <div style="font-size: 12px; color: #80708F;">{{a.project.name}}</div>
+      </div>
+      <div style="font-size: 14px; margin-left: auto; display: flex;">
+        <span>{{600000 | duration}}</span>
+      </div>
+    </div>
+    {% endfor %}
+  </div>
+  {% endif %}
+
 </div>
 {% endblock %}

+ 13 - 1
src/sentry/web/frontend/debug/debug_weekly_report.py

@@ -68,16 +68,27 @@ class DebugWeeklyReportView(MailPreviewView):
                 start_timestamp + (i * ONE_DAY): random.randint(0, daily_maximum)
                 for i in range(0, 7)
             }
+            project_context.replay_count_by_day = {
+                start_timestamp + (i * ONE_DAY): random.randint(0, daily_maximum)
+                for i in range(0, 7)
+            }
+
+            project_context.accepted_error_count = sum(project_context.error_count_by_day.values())
             project_context.accepted_transaction_count = sum(
                 project_context.transaction_count_by_day.values()
             )
-            project_context.accepted_error_count = sum(project_context.error_count_by_day.values())
+            project_context.accepted_replay_count = sum(
+                project_context.replay_count_by_day.values()
+            )
             project_context.dropped_error_count = int(
                 random.weibullvariate(5, 1) * random.paretovariate(0.2)
             )
             project_context.dropped_transaction_count = int(
                 random.weibullvariate(5, 1) * random.paretovariate(0.2)
             )
+            project_context.dropped_replay_count = int(
+                random.weibullvariate(5, 1) * random.paretovariate(0.2)
+            )
             project_context.key_errors = [
                 (g, None, random.randint(0, 1000)) for g in Group.objects.all()[:3]
             ]
@@ -120,6 +131,7 @@ class DebugWeeklyReportView(MailPreviewView):
                 (g, None, random.randint(0, 1000))
                 for g in Group.objects.filter(type__gte=1000, type__lt=2000).all()[:3]
             ]
+
             ctx.projects[project.id] = project_context
 
         return render_template_context(ctx, None)

+ 10 - 0
src/sentry/web/urls.py

@@ -902,6 +902,16 @@ urlpatterns += [
                     react_page_view,
                     name="sentry-organization-stats",
                 ),
+                url(
+                    r"^(?P<organization_slug>[\w_-]+)/replays/$",
+                    react_page_view,
+                    name="sentry-organization-replays",
+                ),
+                url(
+                    r"^(?P<organization_slug>[\w_-]+)/replays/(?P<replay_id>[\w_-]+)/$",
+                    react_page_view,
+                    name="sentry-organization-replay-details",
+                ),
                 url(
                     r"^(?P<organization_slug>[\w_-]+)/restore/$",
                     RestoreOrganizationView.as_view(),

+ 52 - 0
tests/sentry/tasks/test_weekly_reports.py

@@ -430,6 +430,8 @@ class WeeklyReportsTest(OutcomesSnubaTest, SnubaTestCase):
             "color": "#422C6E",
             "dropped_error_count": 2,
             "accepted_error_count": 1,
+            "accepted_replay_count": 0,
+            "dropped_replay_count": 0,
             "dropped_transaction_count": 9,
             "accepted_transaction_count": 3,
         }
@@ -437,6 +439,7 @@ class WeeklyReportsTest(OutcomesSnubaTest, SnubaTestCase):
         assert ctx["trends"]["series"][-2][1][0] == {
             "color": "#422C6E",
             "error_count": 1,
+            "replay_count": 0,
             "transaction_count": 3,
         }
 
@@ -460,3 +463,52 @@ class WeeklyReportsTest(OutcomesSnubaTest, SnubaTestCase):
 
         prepare_organization_report(to_timestamp(now), ONE_DAY * 7, self.organization.id)
         assert mock_send_email.call_count == 0
+
+    @with_feature("organizations:session-replay")
+    @with_feature("organizations:session-replay-weekly-email")
+    @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
+    def test_message_builder_replays(self, message_builder):
+
+        now = timezone.now()
+        two_days_ago = now - timedelta(days=2)
+        timestamp = to_timestamp(floor_to_utc_day(now))
+
+        for outcome, category, num in [
+            (Outcome.ACCEPTED, DataCategory.REPLAY, 6),
+            (Outcome.RATE_LIMITED, DataCategory.REPLAY, 7),
+        ]:
+            self.store_outcomes(
+                {
+                    "org_id": self.organization.id,
+                    "project_id": self.project.id,
+                    "outcome": outcome,
+                    "category": category,
+                    "timestamp": two_days_ago,
+                    "key_id": 1,
+                },
+                num_times=num,
+            )
+
+        prepare_organization_report(timestamp, ONE_DAY * 7, self.organization.id)
+
+        message_params = message_builder.call_args.kwargs
+        ctx = message_params["context"]
+
+        assert ctx["trends"]["legend"][0] == {
+            "slug": "bar",
+            "url": f"http://testserver/organizations/baz/issues/?project={self.project.id}",
+            "color": "#422C6E",
+            "dropped_error_count": 0,
+            "accepted_error_count": 0,
+            "accepted_replay_count": 6,
+            "dropped_replay_count": 7,
+            "dropped_transaction_count": 0,
+            "accepted_transaction_count": 0,
+        }
+
+        assert ctx["trends"]["series"][-2][1][0] == {
+            "color": "#422C6E",
+            "error_count": 0,
+            "replay_count": 6,
+            "transaction_count": 0,
+        }