Skip to content

Commit bb8d3d1

Browse files
authored
Merge pull request #299 from supertokens/dashboard-analytics
feat: analytics api endpoint for dashboard
2 parents d62a4b1 + 9d26b02 commit bb8d3d1

File tree

9 files changed

+145
-156
lines changed

9 files changed

+145
-156
lines changed

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
USER_DELETE = "/user/remove"
3030
USERS = "/users"
3131
TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry"
32-
TELEMETRY_SUPERTOKENS_API_VERSION = "2"
32+
TELEMETRY_SUPERTOKENS_API_VERSION = "3"
3333
ERROR_MESSAGE_KEY = "message"
3434
API_KEY_HEADER = "api-key"
3535
RID_KEY_HEADER = "rid"

supertokens_python/recipe/dashboard/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14+
from .analytics import handle_analytics_post
1415
from .api_key_protector import api_key_protector
1516
from .dashboard import handle_dashboard_api
1617
from .signin import handle_emailpassword_signin_api
@@ -49,4 +50,5 @@
4950
"handle_email_verify_token_post",
5051
"handle_emailpassword_signin_api",
5152
"handle_emailpassword_signout_api",
53+
"handle_analytics_post",
5254
]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
2+
#
3+
# This software is licensed under the Apache License, Version 2.0 (the
4+
# "License") as published by the Apache Software Foundation.
5+
#
6+
# You may not use this file except in compliance with the License. You may
7+
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import TYPE_CHECKING
18+
19+
from httpx import AsyncClient
20+
21+
from supertokens_python import Supertokens
22+
from supertokens_python.constants import (
23+
TELEMETRY_SUPERTOKENS_API_URL,
24+
TELEMETRY_SUPERTOKENS_API_VERSION,
25+
)
26+
from supertokens_python.constants import VERSION as SDKVersion
27+
from supertokens_python.exceptions import raise_bad_input_exception
28+
from supertokens_python.normalised_url_path import NormalisedURLPath
29+
from supertokens_python.querier import Querier
30+
31+
from ..interfaces import AnalyticsResponse
32+
33+
if TYPE_CHECKING:
34+
from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions
35+
36+
37+
async def handle_analytics_post(
38+
_: APIInterface, api_options: APIOptions
39+
) -> AnalyticsResponse:
40+
if not Supertokens.get_instance().telemetry:
41+
return AnalyticsResponse()
42+
body = await api_options.request.json()
43+
if body is None:
44+
raise_bad_input_exception("Please send body")
45+
email = body.get("email")
46+
dashboard_version = body.get("dashboardVersion")
47+
48+
if email is None:
49+
raise_bad_input_exception("Missing required property 'email'")
50+
if dashboard_version is None:
51+
raise_bad_input_exception("Missing required property 'dashboardVersion'")
52+
53+
telemetry_id = None
54+
55+
try:
56+
response = await Querier.get_instance().send_get_request(
57+
NormalisedURLPath("/telemetry")
58+
)
59+
if response is not None:
60+
if (
61+
"exists" in response
62+
and response["exists"]
63+
and "telemetryId" in response
64+
):
65+
telemetry_id = response["telemetryId"]
66+
67+
number_of_users = await Supertokens.get_instance().get_user_count(
68+
include_recipe_ids=None
69+
)
70+
71+
except Exception as __:
72+
# If either telemetry id API or user count fetch fails, no event should be sent
73+
return AnalyticsResponse()
74+
75+
apiDomain, websiteDomain, appName = (
76+
api_options.app_info.api_domain,
77+
api_options.app_info.website_domain,
78+
api_options.app_info.app_name,
79+
)
80+
81+
data = {
82+
"websiteDomain": websiteDomain.get_as_string_dangerous(),
83+
"apiDomain": apiDomain.get_as_string_dangerous(),
84+
"appName": appName,
85+
"sdk": "python",
86+
"sdkVersion": SDKVersion,
87+
"numberOfUsers": number_of_users,
88+
"email": email,
89+
"dashboardVersion": dashboard_version,
90+
}
91+
92+
if telemetry_id is not None:
93+
data["telemetryId"] = telemetry_id
94+
95+
try:
96+
async with AsyncClient() as client:
97+
await client.post( # type: ignore
98+
url=TELEMETRY_SUPERTOKENS_API_URL,
99+
json=data,
100+
headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
101+
)
102+
except Exception as __:
103+
# If telemetry event fails, no error should be thrown
104+
pass
105+
106+
return AnalyticsResponse()

supertokens_python/recipe/dashboard/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"
1111
EMAIL_PASSWORD_SIGN_IN = "/api/signin"
1212
EMAIL_PASSSWORD_SIGNOUT = "/api/signout"
13+
DASHBOARD_ANALYTICS_API = "/api/analytics"

supertokens_python/recipe/dashboard/interfaces.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,10 @@ class SignOutOK(APIResponse):
293293

294294
def to_json(self):
295295
return {"status": self.status}
296+
297+
298+
class AnalyticsResponse(APIResponse):
299+
status: str = "OK"
300+
301+
def to_json(self) -> Dict[str, Any]:
302+
return {"status": self.status}

supertokens_python/recipe/dashboard/recipe.py

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

2222
from .api import (
2323
api_key_protector,
24+
handle_analytics_post,
2425
handle_dashboard_api,
2526
handle_email_verify_token_post,
2627
handle_emailpassword_signin_api,
@@ -53,6 +54,7 @@
5354
from supertokens_python.exceptions import SuperTokensError, raise_general_exception
5455

5556
from .constants import (
57+
DASHBOARD_ANALYTICS_API,
5658
DASHBOARD_API,
5759
EMAIL_PASSSWORD_SIGNOUT,
5860
EMAIL_PASSWORD_SIGN_IN,
@@ -181,6 +183,9 @@ async def handle_api_request(
181183
api_function = handle_email_verify_token_post
182184
elif request_id == EMAIL_PASSSWORD_SIGNOUT:
183185
api_function = handle_emailpassword_signout_api
186+
elif request_id == DASHBOARD_ANALYTICS_API:
187+
if method == "post":
188+
api_function = handle_analytics_post
184189

185190
if api_function is not None:
186191
return await api_key_protector(

supertokens_python/recipe/dashboard/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
from ...normalised_url_path import NormalisedURLPath
5050
from .constants import (
51+
DASHBOARD_ANALYTICS_API,
5152
DASHBOARD_API,
5253
EMAIL_PASSSWORD_SIGNOUT,
5354
EMAIL_PASSWORD_SIGN_IN,
@@ -237,6 +238,8 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]:
237238
return EMAIL_PASSWORD_SIGN_IN
238239
if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post":
239240
return EMAIL_PASSSWORD_SIGNOUT
241+
if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post":
242+
return DASHBOARD_ANALYTICS_API
240243

241244
return None
242245

supertokens_python/supertokens.py

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,14 @@
1414

1515
from __future__ import annotations
1616

17+
from os import environ
1718
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union
1819

1920
from typing_extensions import Literal
2021

2122
from supertokens_python.logger import get_maybe_none_as_str, log_debug_message
2223

23-
from .constants import (
24-
FDI_KEY_HEADER,
25-
RID_KEY_HEADER,
26-
TELEMETRY,
27-
TELEMETRY_SUPERTOKENS_API_URL,
28-
TELEMETRY_SUPERTOKENS_API_VERSION,
29-
USER_COUNT,
30-
USER_DELETE,
31-
USERS,
32-
)
24+
from .constants import FDI_KEY_HEADER, RID_KEY_HEADER, USER_COUNT, USER_DELETE, USERS
3325
from .exceptions import SuperTokensError
3426
from .interfaces import (
3527
CreateUserIdMappingOkResult,
@@ -47,12 +39,11 @@
4739
from .querier import Querier
4840
from .types import ThirdPartyInfo, User, UsersResponse
4941
from .utils import (
50-
execute_async,
5142
get_rid_from_header,
43+
get_top_level_domain_for_same_site_resolution,
5244
is_version_gte,
5345
normalise_http_method,
5446
send_non_200_response_with_message,
55-
get_top_level_domain_for_same_site_resolution,
5647
)
5748

5849
if TYPE_CHECKING:
@@ -62,9 +53,6 @@
6253
from supertokens_python.recipe.session import SessionContainer
6354

6455
import json
65-
from os import environ
66-
67-
from httpx import AsyncClient
6856

6957
from .exceptions import BadInputError, GeneralError, raise_general_exception
7058

@@ -202,55 +190,11 @@ def __init__(
202190
map(lambda func: func(self.app_info), recipe_list)
203191
)
204192

205-
if telemetry is None:
206-
# If telemetry is not provided, enable it by default for production environment
207-
telemetry = ("SUPERTOKENS_ENV" not in environ) or (
208-
environ["SUPERTOKENS_ENV"] != "testing"
209-
)
210-
211-
if telemetry:
212-
try:
213-
execute_async(self.app_info.mode, self.send_telemetry)
214-
except Exception:
215-
pass # Do not stop app startup if telemetry fails
216-
217-
async def send_telemetry(self):
218-
# If telemetry is enabled manually and the app is running in testing mode,
219-
# do not send the telemetry
220-
skip_telemetry = ("SUPERTOKENS_ENV" in environ) and (
221-
environ["SUPERTOKENS_ENV"] == "testing"
193+
self.telemetry = (
194+
telemetry
195+
if telemetry is not None
196+
else (environ.get("TEST_MODE") != "testing")
222197
)
223-
if skip_telemetry:
224-
self._telemetry_status = "SKIPPED"
225-
return
226-
227-
try:
228-
querier = Querier.get_instance(None)
229-
response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {})
230-
telemetry_id = None
231-
if (
232-
"exists" in response
233-
and response["exists"]
234-
and "telemetryId" in response
235-
):
236-
telemetry_id = response["telemetryId"]
237-
data = {
238-
"appName": self.app_info.app_name,
239-
"websiteDomain": self.app_info.website_domain.get_as_string_dangerous(),
240-
"sdk": "python",
241-
}
242-
if telemetry_id is not None:
243-
data = {**data, "telemetryId": telemetry_id}
244-
async with AsyncClient() as client:
245-
await client.post( # type: ignore
246-
url=TELEMETRY_SUPERTOKENS_API_URL,
247-
json=data,
248-
headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
249-
)
250-
251-
self._telemetry_status = "SUCCESS"
252-
except Exception:
253-
self._telemetry_status = "EXCEPTION"
254198

255199
@staticmethod
256200
def init(

0 commit comments

Comments
 (0)