Skip to content

Commit 63ead02

Browse files
authored
feat(app-platform): Analytics (#12718)
Adds analytics recording for a set of events: - Sentry App Created - Sentry App Updated - Sentry App Deleted - Sentry App Installed - Sentry App Uninstalled - Sentry App Token Exchanged (authorize) - Sentry App Token Exchanged (refresh)
1 parent a652dda commit 63ead02

26 files changed

+289
-13
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppCreatedEvent(analytics.Event):
7+
type = 'sentry_app.created'
8+
9+
attributes = (
10+
analytics.Attribute('user_id'),
11+
analytics.Attribute('organization_id'),
12+
analytics.Attribute('sentry_app'),
13+
)
14+
15+
16+
analytics.register(SentryAppCreatedEvent)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppDeletedEvent(analytics.Event):
7+
type = 'sentry_app.deleted'
8+
9+
attributes = (
10+
analytics.Attribute('user_id'),
11+
analytics.Attribute('organization_id'),
12+
analytics.Attribute('sentry_app'),
13+
)
14+
15+
16+
analytics.register(SentryAppDeletedEvent)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppInstalledEvent(analytics.Event):
7+
type = 'sentry_app.installed'
8+
9+
attributes = (
10+
analytics.Attribute('user_id'),
11+
analytics.Attribute('organization_id'),
12+
analytics.Attribute('sentry_app'),
13+
)
14+
15+
16+
analytics.register(SentryAppInstalledEvent)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppTokenExchangedEvent(analytics.Event):
7+
type = 'sentry_app.token_exchanged'
8+
9+
attributes = (
10+
analytics.Attribute('sentry_app_installation_id'),
11+
analytics.Attribute('exchange_type'),
12+
)
13+
14+
15+
analytics.register(SentryAppTokenExchangedEvent)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppUninstalledEvent(analytics.Event):
7+
type = 'sentry_app.uninstalled'
8+
9+
attributes = (
10+
analytics.Attribute('user_id'),
11+
analytics.Attribute('organization_id'),
12+
analytics.Attribute('sentry_app'),
13+
)
14+
15+
16+
analytics.register(SentryAppUninstalledEvent)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppUpdatedEvent(analytics.Event):
7+
type = 'sentry_app.updated'
8+
9+
attributes = (
10+
analytics.Attribute('user_id'),
11+
analytics.Attribute('sentry_app'),
12+
)
13+
14+
15+
analytics.register(SentryAppUpdatedEvent)

src/sentry/api/endpoints/sentry_app_details.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def put(self, request, sentry_app):
3737
result = serializer.object
3838

3939
updated_app = Updater.run(
40+
user=request.user,
4041
sentry_app=sentry_app,
4142
name=result.get('name'),
4243
author=result.get('author'),
@@ -61,6 +62,7 @@ def delete(self, request, sentry_app):
6162

6263
if sentry_app.status == SentryAppStatus.UNPUBLISHED:
6364
Destroyer.run(
65+
user=request.user,
6466
sentry_app=sentry_app,
6567
request=request,
6668
)

src/sentry/api/endpoints/sentry_apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def post(self, request, organization):
3131

3232
sentry_app = Creator.run(
3333
name=result.get('name'),
34+
user=request.user,
3435
author=result.get('author'),
3536
organization=self._get_user_org(request),
3637
scopes=result.get('scopes'),

src/sentry/mediators/mediator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def run(cls, *args, **kwargs):
160160
with obj.log():
161161
result = obj.call()
162162
obj.audit()
163+
obj.record_analytics()
163164
return result
164165

165166
def __init__(self, *args, **kwargs):
@@ -172,6 +173,10 @@ def audit(self):
172173
# used for creating audit log entries
173174
pass
174175

176+
def record_analytics(self):
177+
# used to record data to Amplitude
178+
pass
179+
175180
def call(self):
176181
raise NotImplementedError
177182

src/sentry/mediators/sentry_app_installations/creator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import six
44

5+
from sentry import analytics
56
from sentry.mediators import Mediator, Param, service_hooks
67
from sentry.models import (
78
AuditLogEntryEvent, ApiAuthorization, ApiGrant, SentryApp, SentryAppInstallation
@@ -72,6 +73,14 @@ def audit(self):
7273
},
7374
)
7475

76+
def record_analytics(self):
77+
analytics.record(
78+
'sentry_app.installed',
79+
user_id=self.user.id,
80+
organization_id=self.organization.id,
81+
sentry_app=self.slug,
82+
)
83+
7584
@memoize
7685
def api_application(self):
7786
return self.sentry_app.application

src/sentry/mediators/sentry_app_installations/destroyer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
from sentry import analytics
34
from sentry.mediators import Mediator, Param
45
from sentry.mediators import service_hooks
56
from sentry.models import AuditLogEntryEvent, ServiceHook
@@ -54,3 +55,11 @@ def audit(self):
5455
'sentry_app': self.install.sentry_app.name,
5556
},
5657
)
58+
59+
def record_analytics(self):
60+
analytics.record(
61+
'sentry_app.uninstalled',
62+
user_id=self.user.id,
63+
organization_id=self.install.organization_id,
64+
sentry_app=self.install.sentry_app.slug,
65+
)

src/sentry/mediators/sentry_apps/creator.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from collections import Iterable
66

7+
from sentry import analytics
78
from sentry.utils.audit import create_audit_entry
89
from sentry.mediators import Mediator, Param
910
from sentry.models import (AuditLogEntryEvent, ApiApplication, SentryApp, SentryAppComponent, User,)
@@ -21,13 +22,14 @@ class Creator(Mediator):
2122
schema = Param(dict, default=lambda self: {})
2223
overview = Param(six.string_types, required=False)
2324
request = Param('rest_framework.request.Request', required=False)
25+
user = Param('sentry.models.User')
2426

2527
def call(self):
2628
self.proxy = self._create_proxy_user()
2729
self.api_app = self._create_api_application()
28-
self.app = self._create_sentry_app()
30+
self.sentry_app = self._create_sentry_app()
2931
self._create_ui_components()
30-
return self.app
32+
return self.sentry_app
3133

3234
def _create_proxy_user(self):
3335
return User.objects.create(
@@ -64,7 +66,7 @@ def _create_ui_components(self):
6466
for element in schema.get('elements', []):
6567
SentryAppComponent.objects.create(
6668
type=element['type'],
67-
sentry_app_id=self.app.id,
69+
sentry_app_id=self.sentry_app.id,
6870
schema=element,
6971
)
7072

@@ -76,6 +78,14 @@ def audit(self):
7678
target_object=self.organization.id,
7779
event=AuditLogEntryEvent.SENTRY_APP_ADD,
7880
data={
79-
'sentry_app': self.app.name,
81+
'sentry_app': self.sentry_app.name,
8082
},
8183
)
84+
85+
def record_analytics(self):
86+
analytics.record(
87+
'sentry_app.created',
88+
user_id=self.user.id,
89+
organization_id=self.organization.id,
90+
sentry_app=self.sentry_app.slug,
91+
)

src/sentry/mediators/sentry_apps/destroyer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
from sentry import analytics
34
from sentry.mediators import Mediator, Param
45
from sentry.mediators import sentry_app_installations
56
from sentry.models import AuditLogEntryEvent
@@ -44,3 +45,11 @@ def audit(self):
4445
'sentry_app': self.sentry_app.name,
4546
},
4647
)
48+
49+
def record_analytics(self):
50+
analytics.record(
51+
'sentry_app.deleted',
52+
user_id=self.user.id,
53+
organization_id=self.sentry_app.owner.id,
54+
sentry_app=self.sentry_app.slug,
55+
)

src/sentry/mediators/sentry_apps/updater.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import six
44

55
from collections import Iterable
6-
from sentry.coreapi import APIError
76

7+
from sentry import analytics
8+
from sentry.coreapi import APIError
89
from sentry.constants import SentryAppStatus
910
from sentry.mediators import Mediator, Param
1011
from sentry.mediators import service_hooks
@@ -23,6 +24,7 @@ class Updater(Mediator):
2324
is_alertable = Param(bool, required=False)
2425
schema = Param(dict, required=False)
2526
overview = Param(six.string_types, required=False)
27+
user = Param('sentry.models.User')
2628

2729
def call(self):
2830
self._update_name()
@@ -103,3 +105,10 @@ def _create_ui_components(self):
103105
sentry_app_id=self.sentry_app.id,
104106
schema=element,
105107
)
108+
109+
def record_analytics(self):
110+
analytics.record(
111+
'sentry_app.updated',
112+
user_id=self.user.id,
113+
sentry_app=self.sentry_app.slug,
114+
)

src/sentry/mediators/token_exchange/grant_exchanger.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from datetime import datetime
77

8+
from sentry import analytics
89
from sentry.coreapi import APIUnauthorized
910
from sentry.mediators import Mediator, Param
1011
from sentry.mediators.token_exchange.validator import Validator
@@ -29,13 +30,22 @@ def call(self):
2930
# exchangable, so we delete it.
3031
self._delete_grant()
3132

32-
return ApiToken.objects.create(
33+
self.token = ApiToken.objects.create(
3334
user=self.user,
3435
application=self.application,
3536
scope_list=self.sentry_app.scope_list,
3637
expires_at=token_expiration()
3738
)
3839

40+
return self.token
41+
42+
def record_analytics(self):
43+
analytics.record(
44+
'sentry_app.token_exchanged',
45+
sentry_app_installation_id=self.install.id,
46+
exchange_type='authorization',
47+
)
48+
3949
def _validate(self):
4050
Validator.run(
4151
install=self.install,

src/sentry/mediators/token_exchange/refresher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from datetime import datetime
77

8+
from sentry import analytics
89
from sentry.coreapi import APIUnauthorized
910
from sentry.mediators import Mediator, Param
1011
from sentry.mediators.token_exchange.validator import Validator
@@ -34,6 +35,13 @@ def call(self):
3435
expires_at=token_expiration(),
3536
)
3637

38+
def record_analytics(self):
39+
analytics.record(
40+
'sentry_app.token_exchanged',
41+
sentry_app_installation_id=self.install.id,
42+
exchange_type='refresh',
43+
)
44+
3745
def _validate(self):
3846
Validator.run(
3947
install=self.install,

src/sentry/testutils/factories.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ def add_user_permission(user, permission):
673673

674674
@staticmethod
675675
def create_sentry_app(name=None, author='Sentry', organization=None, published=False, scopes=(),
676-
webhook_url=None, **kwargs):
676+
webhook_url=None, user=None, **kwargs):
677677
if not name:
678678
name = petname.Generate(2, ' ', letters=10).title()
679679
if not organization:
@@ -682,6 +682,7 @@ def create_sentry_app(name=None, author='Sentry', organization=None, published=F
682682
webhook_url = 'https://example.com/webhook'
683683

684684
_kwargs = {
685+
'user': (user or Factories.create_user()),
685686
'name': name,
686687
'organization': organization,
687688
'author': author,

tests/sentry/api/endpoints/test_sentry_app_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_cannot_update_name_with_non_unique_slug(self):
158158
organization=self.org,
159159
)
160160

161-
sentry_apps.Destroyer.run(sentry_app=sentry_app)
161+
sentry_apps.Destroyer.run(sentry_app=sentry_app, user=self.user)
162162

163163
response = self.client.put(
164164
self.url,

tests/sentry/api/endpoints/test_sentry_apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def test_non_unique_app_slug(self):
140140
name='Foo Bar',
141141
organization=self.org,
142142
)
143-
sentry_apps.Destroyer.run(sentry_app=sentry_app)
143+
sentry_apps.Destroyer.run(sentry_app=sentry_app, user=self.user)
144144
response = self._post(**{'name': sentry_app.name})
145145
assert response.status_code == 422
146146
assert response.data == \

tests/sentry/mediators/sentry_app_installations/test_creator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,19 @@ def test_associations(self):
9797

9898
assert install.api_grant is not None
9999
assert install.authorization is not None
100+
101+
@patch('sentry.analytics.record')
102+
def test_records_analytics(self, record):
103+
Creator.run(
104+
organization=self.org,
105+
slug='nulldb',
106+
user=self.user,
107+
request=self.make_request(user=self.user, method='GET'),
108+
)
109+
110+
record.assert_called_with(
111+
'sentry_app.installed',
112+
user_id=self.user.id,
113+
organization_id=self.org.id,
114+
sentry_app='nulldb',
115+
)

0 commit comments

Comments
 (0)