Skip to content

feat: analytics api endpoint for dashboard #299

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

Merged
merged 5 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion supertokens_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
USER_DELETE = "/user/remove"
USERS = "/users"
TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry"
TELEMETRY_SUPERTOKENS_API_VERSION = "2"
TELEMETRY_SUPERTOKENS_API_VERSION = "3"
ERROR_MESSAGE_KEY = "message"
API_KEY_HEADER = "api-key"
RID_KEY_HEADER = "rid"
Expand Down
2 changes: 2 additions & 0 deletions supertokens_python/recipe/dashboard/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from .analytics import handle_analytics_post
from .api_key_protector import api_key_protector
from .dashboard import handle_dashboard_api
from .signin import handle_emailpassword_signin_api
Expand Down Expand Up @@ -49,4 +50,5 @@
"handle_email_verify_token_post",
"handle_emailpassword_signin_api",
"handle_emailpassword_signout_api",
"handle_analytics_post",
]
106 changes: 106 additions & 0 deletions supertokens_python/recipe/dashboard/api/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
#
# This software is licensed under the Apache License, Version 2.0 (the
# "License") as published by the Apache Software Foundation.
#
# You may not use this file except in compliance with the License. You may
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import annotations

from typing import TYPE_CHECKING

from httpx import AsyncClient

from supertokens_python import Supertokens
from supertokens_python.constants import (
TELEMETRY_SUPERTOKENS_API_URL,
TELEMETRY_SUPERTOKENS_API_VERSION,
)
from supertokens_python.constants import VERSION as SDKVersion
from supertokens_python.exceptions import raise_bad_input_exception
from supertokens_python.normalised_url_path import NormalisedURLPath
from supertokens_python.querier import Querier

from ..interfaces import AnalyticsResponse

if TYPE_CHECKING:
from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions


async def handle_analytics_post(
_: APIInterface, api_options: APIOptions
) -> AnalyticsResponse:
if not Supertokens.get_instance().telemetry:
return AnalyticsResponse()
body = await api_options.request.json()
if body is None:
raise_bad_input_exception("Please send body")
email = body.get("email")
dashboard_version = body.get("dashboardVersion")

if email is None:
raise_bad_input_exception("Missing required property 'email'")
if dashboard_version is None:
raise_bad_input_exception("Missing required property 'dashboardVersion'")

telemetry_id = None

try:
response = await Querier.get_instance().send_get_request(
NormalisedURLPath("/telemetry")
)
if response is not None:
if (
"exists" in response
and response["exists"]
and "telemetryId" in response
):
telemetry_id = response["telemetryId"]

number_of_users = await Supertokens.get_instance().get_user_count(
include_recipe_ids=None
)

except Exception as __:
# If either telemetry id API or user count fetch fails, no event should be sent
return AnalyticsResponse()

apiDomain, websiteDomain, appName = (
api_options.app_info.api_domain,
api_options.app_info.website_domain,
api_options.app_info.app_name,
)

data = {
"websiteDomain": websiteDomain.get_as_string_dangerous(),
"apiDomain": apiDomain.get_as_string_dangerous(),
"appName": appName,
"sdk": "python",
"sdkVersion": SDKVersion,
"numberOfUsers": number_of_users,
"email": email,
"dashboardVersion": dashboard_version,
}

if telemetry_id is not None:
data["telemetryId"] = telemetry_id

try:
async with AsyncClient() as client:
await client.post( # type: ignore
url=TELEMETRY_SUPERTOKENS_API_URL,
json=data,
headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
)
except Exception as __:
# If telemetry event fails, no error should be thrown
pass

return AnalyticsResponse()
1 change: 1 addition & 0 deletions supertokens_python/recipe/dashboard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"
EMAIL_PASSWORD_SIGN_IN = "/api/signin"
EMAIL_PASSSWORD_SIGNOUT = "/api/signout"
DASHBOARD_ANALYTICS_API = "/api/analytics"
7 changes: 7 additions & 0 deletions supertokens_python/recipe/dashboard/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,10 @@ class SignOutOK(APIResponse):

def to_json(self):
return {"status": self.status}


class AnalyticsResponse(APIResponse):
status: str = "OK"

def to_json(self) -> Dict[str, Any]:
return {"status": self.status}
5 changes: 5 additions & 0 deletions supertokens_python/recipe/dashboard/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from .api import (
api_key_protector,
handle_analytics_post,
handle_dashboard_api,
handle_email_verify_token_post,
handle_emailpassword_signin_api,
Expand Down Expand Up @@ -53,6 +54,7 @@
from supertokens_python.exceptions import SuperTokensError, raise_general_exception

from .constants import (
DASHBOARD_ANALYTICS_API,
DASHBOARD_API,
EMAIL_PASSSWORD_SIGNOUT,
EMAIL_PASSWORD_SIGN_IN,
Expand Down Expand Up @@ -181,6 +183,9 @@ async def handle_api_request(
api_function = handle_email_verify_token_post
elif request_id == EMAIL_PASSSWORD_SIGNOUT:
api_function = handle_emailpassword_signout_api
elif request_id == DASHBOARD_ANALYTICS_API:
if method == "post":
api_function = handle_analytics_post

if api_function is not None:
return await api_key_protector(
Expand Down
3 changes: 3 additions & 0 deletions supertokens_python/recipe/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

from ...normalised_url_path import NormalisedURLPath
from .constants import (
DASHBOARD_ANALYTICS_API,
DASHBOARD_API,
EMAIL_PASSSWORD_SIGNOUT,
EMAIL_PASSWORD_SIGN_IN,
Expand Down Expand Up @@ -237,6 +238,8 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]:
return EMAIL_PASSWORD_SIGN_IN
if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post":
return EMAIL_PASSSWORD_SIGNOUT
if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post":
return DASHBOARD_ANALYTICS_API

return None

Expand Down
70 changes: 7 additions & 63 deletions supertokens_python/supertokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,14 @@

from __future__ import annotations

from os import environ
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union

from typing_extensions import Literal

from supertokens_python.logger import get_maybe_none_as_str, log_debug_message

from .constants import (
FDI_KEY_HEADER,
RID_KEY_HEADER,
TELEMETRY,
TELEMETRY_SUPERTOKENS_API_URL,
TELEMETRY_SUPERTOKENS_API_VERSION,
USER_COUNT,
USER_DELETE,
USERS,
)
from .constants import FDI_KEY_HEADER, RID_KEY_HEADER, USER_COUNT, USER_DELETE, USERS
from .exceptions import SuperTokensError
from .interfaces import (
CreateUserIdMappingOkResult,
Expand All @@ -47,12 +39,11 @@
from .querier import Querier
from .types import ThirdPartyInfo, User, UsersResponse
from .utils import (
execute_async,
get_rid_from_header,
get_top_level_domain_for_same_site_resolution,
is_version_gte,
normalise_http_method,
send_non_200_response_with_message,
get_top_level_domain_for_same_site_resolution,
)

if TYPE_CHECKING:
Expand All @@ -62,9 +53,6 @@
from supertokens_python.recipe.session import SessionContainer

import json
from os import environ

from httpx import AsyncClient

from .exceptions import BadInputError, GeneralError, raise_general_exception

Expand Down Expand Up @@ -202,55 +190,11 @@ def __init__(
map(lambda func: func(self.app_info), recipe_list)
)

if telemetry is None:
# If telemetry is not provided, enable it by default for production environment
telemetry = ("SUPERTOKENS_ENV" not in environ) or (
environ["SUPERTOKENS_ENV"] != "testing"
)

if telemetry:
try:
execute_async(self.app_info.mode, self.send_telemetry)
except Exception:
pass # Do not stop app startup if telemetry fails

async def send_telemetry(self):
# If telemetry is enabled manually and the app is running in testing mode,
# do not send the telemetry
skip_telemetry = ("SUPERTOKENS_ENV" in environ) and (
environ["SUPERTOKENS_ENV"] == "testing"
self.telemetry = (
telemetry
if telemetry is not None
else (environ.get("TEST_MODE") != "testing")
)
if skip_telemetry:
self._telemetry_status = "SKIPPED"
return

try:
querier = Querier.get_instance(None)
response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {})
telemetry_id = None
if (
"exists" in response
and response["exists"]
and "telemetryId" in response
):
telemetry_id = response["telemetryId"]
data = {
"appName": self.app_info.app_name,
"websiteDomain": self.app_info.website_domain.get_as_string_dangerous(),
"sdk": "python",
}
if telemetry_id is not None:
data = {**data, "telemetryId": telemetry_id}
async with AsyncClient() as client:
await client.post( # type: ignore
url=TELEMETRY_SUPERTOKENS_API_URL,
json=data,
headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
)

self._telemetry_status = "SUCCESS"
except Exception:
self._telemetry_status = "EXCEPTION"

@staticmethod
def init(
Expand Down
Loading