Skip to content
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

Merged
merged 18 commits into from
Oct 28, 2021

Conversation

NisanthanNanthakumar
Copy link
Contributor

Objective:

We want to add logging to Big Query to track adoption metrics of Alert Rule UI Components.

@NisanthanNanthakumar NisanthanNanthakumar requested review from a team October 25, 2021 18:29
@@ -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
Copy link
Member

@evanpurkhiser evanpurkhiser Oct 25, 2021

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),
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

@scefali scefali Oct 25, 2021

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

Copy link
Contributor Author

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),
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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"),
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Contributor Author

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:
Copy link
Contributor

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

@scefali
Copy link
Contributor

scefali commented Oct 25, 2021

@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),
Copy link
Contributor

@scefali scefali Oct 25, 2021

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...

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@@ -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":
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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:
Copy link
Contributor

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(
Copy link
Contributor

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.

Copy link
Contributor Author

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"
Copy link
Contributor

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", [])
    )
),

Copy link
Contributor

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))
Copy link
Contributor

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", {})
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Contributor

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]

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants