-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(alert-rule): Instrument analytics for Alert Rule UI Components #29552
Conversation
src/sentry/tasks/sentry_apps.py
Outdated
@@ -472,4 +471,56 @@ def send_and_save_webhook_request(sentry_app, app_platform_event, url=None): | |||
|
|||
resp.raise_for_status() | |||
|
|||
# On success, record BigQuery analytic event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember sentry is an open source code base, while we might be using big query, it's not true that on premise would be using this.
So I would recommend avoiding mentioning 'big query' and just say record analytic event
type = "alert_rule_ui_component_webhook.sent" | ||
|
||
attributes = ( | ||
analytics.Attribute("installed_org_id", type=str, required=True), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar Can we just call this organization_id
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali hmm I think its better if its more specific, because organization_id
can also refer to the organization receiving the webhook
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar If you add a new field to represent the organization_id
, you're going to have to update our ETL pipeline so we can read the right field when we push to Amplitude:
https://github.com/getsentry/etl/blob/master/etl/operators/amplitude_insert_analytics_events.py#L77-L80
Also, it makes it harder for analytics if some events don't have an organization_id
matching our expected field name. Tilllman and other data folks will have to add special logic for just this event. Unless the event itself has two different organizations, then I would highly recommend just using organization_id
to represent the org
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali 👍 I'll just add a comment then
@@ -8,6 +8,7 @@ class SentryAppCreatedEvent(analytics.Event): | |||
analytics.Attribute("user_id"), | |||
analytics.Attribute("organization_id"), | |||
analytics.Attribute("sentry_app"), | |||
analytics.Attribute("created_alert_rule_ui_component", type=bool, required=False), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar can we make this field a string instead of a boolean? That way if we ever need a third type to distinguish, we don't need a new field and can just make a new value for the string. Booleans are generally not ideal for analytics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali yea Im ok with making it a string type. But the name seems pretty binary, idk if other values can fit without being confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar yea...maybe so. But I think my other comment will impact this: #29552 (comment)
attributes = (analytics.Attribute("user_id"), analytics.Attribute("sentry_app")) | ||
attributes = ( | ||
analytics.Attribute("user_id"), | ||
analytics.Attribute("sentry_app"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar do we really not have an organization_id
here? IMO it's worth adding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali We can get organization_id
from sentry_sentryapp.owner_id
if we have the sentry_app.id
. I think it might be unnecessary when its on the table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar yep, but ETL won't handle this properly when sending to Amplitude unless we change the ETL pipeline
@@ -143,7 +150,13 @@ def post(self, request, project): | |||
sender=self, | |||
is_api_token=request.auth is not None, | |||
) | |||
|
|||
if created_alert_rule_ui_component: | |||
analytics.record( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar why are we making a new event for this instead of just modifying the existing alert rule event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali ahh just realized we use signals for analytics too...
trigger=self.context["trigger"], **validated_data | ||
) | ||
except InvalidTriggerActionError as e: | ||
raise serializers.ValidationError(force_text(e)) | ||
except ApiRateLimitedError as e: | ||
raise serializers.ValidationError(force_text(e)) | ||
else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar Nit: you don't need an else condition here as the other conditions will just raise an exception
@NisanthanNanthakumar what about getting these events into Amplitude as well? |
@@ -11,6 +11,7 @@ class AlertCreatedEvent(analytics.Event): | |||
analytics.Attribute("rule_id"), | |||
analytics.Attribute("rule_type"), | |||
analytics.Attribute("is_api_token"), | |||
analytics.Attribute("has_alert_rule_ui_component", type=str, required=False), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar Maybe this could be a type field? Something a bit less specific than has_alert_rule_ui_component
which implies a boolean? Not sure...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali what do you mean by type field
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar Some sort of field that says why the alert rule was created? So the value could be ui_component
in this case. I'm not sure what field name makes sense though.
src/sentry/tasks/sentry_apps.py
Outdated
@@ -472,4 +471,38 @@ def send_and_save_webhook_request(sentry_app, app_platform_event, url=None): | |||
|
|||
resp.raise_for_status() | |||
|
|||
# On success, record analytic event | |||
# Handle Metric Alerts | |||
if app_platform_event.resource == "metric_alert": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar I would probably move all of the analytics logic to the caller function which will be different for metric alerts, issue alerts, etc. I think it will be a lot more clean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scefali hmm I wanted to record the analytic event after the webhook was successfully sent, incase we get >=400 status codes. Maybe I'll move into a helper function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NisanthanNanthakumar if you put the analytics recording after calling send_and_save_webhook_request
, it wouldn't get called on 400 status codes because we call raise_for_status
@@ -25,6 +25,7 @@ | |||
def trigger_alert_rule_action_creators( | |||
actions: Sequence[Mapping[str, str]], | |||
) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns bool
now.
trigger=self.context["trigger"], **validated_data | ||
) | ||
analytics.record( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I try to keep only lines that can throw in the try/catch block. It prevents other exceptions from being swallowed and it makes it obvious at invocation which line can actually throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgaeta hmm yea originally I had it in an else block. I can revert.
@@ -130,4 +130,8 @@ def record_analytics(self): | |||
user_id=self.user.id, | |||
organization_id=self.organization.id, | |||
sentry_app=self.sentry_app.slug, | |||
created_alert_rule_ui_component="True" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok this might be overkill but you could make this
created_alert_rule_ui_component=str(
any(
element["type"] == "alert-rule-action"
for element in self.schema.get("elements", [])
)
),
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or make a new function:
def get_schema_types(self) -> Set[str]:
return set(element["type"] for element in self.schema.get("elements", []))
so that you can do
created_alert_rule_ui_component=str("alert-rule-action" in self.get_schema_types()),
because you do something similar in the updater.
"type", flat=True | ||
) | ||
elements = [element["type"] for element in self.schema.get("elements", [])] | ||
return list(set(elements) - set(current)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah might as well return this as a Set
.
# Loop through the triggers for the alert rule event. For each trigger, check if an action is an alert rule UI Component | ||
alert_rule_action_ui_component = False | ||
for trigger in ( | ||
app_platform_event.get("data", {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
triggers = (
getattr(app_platform_event, "data", {})
.get("metric_alert", {})
.get("alert_rule", {})
.get("triggers", [])
)
for trigger in triggers:
...
.get("alert_rule", {}) | ||
.get("triggers", []) | ||
): | ||
alert_rule_action_ui_component = next( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just halt iteration once you've found the first one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thinks this makes more sense as a comprehension
actions = [
action
for trigger in triggers
for action in trigger["actions"]
if (action["type"] == "sentry_app" and action["settings"] is not None)
]
if not actions:
return False
return actions[0]
76a3fcf
to
fa6924a
Compare
Objective:
We want to add logging to Big Query to track adoption metrics of Alert Rule UI Components.