Skip to content

Commit

Permalink
Adds analytics endpoint (#505)
Browse files Browse the repository at this point in the history
* Removed flask-selfdoc from project.

* Update score_nodes.py

added relatedPersonalValues to the GET response of the /feed endpoint to include all the personal values associated with each climate change impact for user's feed.

* run linting

* optional parameter to skip recaptcha

* change spelling

* remove timedelta

* Update installation.md

instructions how to free up port 5000 on macs.

* Cm 499 delete account and data (#501)

* Black formatting

* Extra test to ensure deleted user can't login

* added api documentation

* #499 ondelete="SET NULL" or "CASCADE"

* #499 explicit ondelete action for user foreign keys

* Change route method from POST to DELETE

* switch POST documentation to DELETE in app/static/Climate-Mind_bundled.yml

---------

Co-authored-by: Daniil Mashkin <[email protected]>
Co-authored-by: Daniil Mashkin <[email protected]>

* Analytics endpoint (#504)

* Initial commit for #503

* Modified 2 files

* Fixing some bugs

* Remove init not needed

Remove

* Fixed bugs

* clearer datetime string format documentation.

* Fixed bug with date time

* Lint

* #503 unittests fixed

---------

Co-authored-by: Daniil Mashkin <[email protected]>

---------

Co-authored-by: Jason Hutson <[email protected]>
Co-authored-by: Svenstar74 <[email protected]>
Co-authored-by: Daniil Mashkin <[email protected]>
Co-authored-by: Daniil Mashkin <[email protected]>
  • Loading branch information
5 people authored Nov 24, 2023
1 parent 6b2d435 commit 6a807ba
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 24 deletions.
95 changes: 75 additions & 20 deletions .stoplight/styleguide.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def create_app(config_class=DevelopmentConfig):

app.register_blueprint(ontology_bp)

from app.analytics import bp as analytics_bp

app.register_blueprint(analytics_bp)

return app


Expand Down
5 changes: 5 additions & 0 deletions app/analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import Blueprint

bp = Blueprint("analytics", __name__)

from app.analytics import routes
30 changes: 30 additions & 0 deletions app/analytics/analytics_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import uuid
from app import db
from app.errors.errors import DatabaseError
from app.models import AnalyticsData
from datetime import datetime, timezone


def log_user_a_event(
session_uuid, category, action, label, event_value, event_timestamp, page_url
):
"""
Log an event in the user a analytics data table.
"""
try:
event_to_add = AnalyticsData()

# event_to_add.event_log_uuid = uuid.uuid4() #this should not be needed as autoincrement is set to true.
event_to_add.session_uuid = session_uuid
event_to_add.category = category
event_to_add.action = action
event_to_add.label = label
event_to_add.value = event_value
event_to_add.event_timestamp = event_timestamp
event_to_add.page_url = page_url
db.session.add(event_to_add)
db.session.commit()
except:
raise DatabaseError(
message="An error occurred while logging a user analytics event."
)
53 changes: 53 additions & 0 deletions app/analytics/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from app.analytics import bp
from app.analytics.analytics_logging import log_user_a_event
from app.common.uuid import validate_uuid, uuidType, check_uuid_in_db
from flask_cors import cross_origin
from flask import request, jsonify

import uuid
from datetime import datetime
from app.analytics.schemas import (
AnalyticsSchema,
)
from app.models import AnalyticsData


@bp.route("/analytics", methods=["POST"])
@cross_origin()
def post_user_a_event():
"""
Logs a user a event in the analytics_data table for analytics tracking.
The required request body must include category, action, label, session_uuid, event_timestamp, value, page_url
Session uuid validation are included for accurate logging.
Parameters
==========
(implicitly session_uuid), category, action, label, session_uuid, event_timestamp, value, page_url
Returns
==========
JSON - success message
"""
session_uuid = request.headers.get("X-Session-Id")
session_uuid = validate_uuid(session_uuid, uuidType.SESSION)
check_uuid_in_db(session_uuid, uuidType.SESSION)

json_data = request.get_json(force=True, silent=True)
schema = AnalyticsSchema()
result_data = schema.load(json_data)
log_user_a_event(
session_uuid,
result_data["category"],
result_data["action"],
result_data["label"],
result_data["event_value"],
result_data["event_timestamp"],
result_data["page_url"],
)

response = {"message": "User event logged."}

return jsonify(response), 201
17 changes: 17 additions & 0 deletions app/analytics/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from marshmallow import (
Schema,
fields,
validates_schema,
ValidationError,
)

from app.common.schemas import CamelCaseSchema


class AnalyticsSchema(CamelCaseSchema, Schema):
category = fields.Str(required=True)
action = fields.Str(required=True)
label = fields.Str(required=True)
event_value = fields.Str(required=True)
event_timestamp = fields.DateTime(required=True)
page_url = fields.Str(required=True)
40 changes: 40 additions & 0 deletions app/analytics/tests/test_analytics_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from datetime import datetime, timedelta, timezone

import pytest
import typing
from flask import url_for
from flask.testing import FlaskClient
from flask_jwt_extended import create_access_token
from freezegun import freeze_time
from mock import mock

from app.factories import (
UsersFactory,
faker,
SessionsFactory,
PasswordResetLinkFactory,
ScoresFactory,
)
from app.models import AnalyticsData, Users


@pytest.mark.integration
def test_add_event(client, accept_json):
session = SessionsFactory()
session_header = [("X-Session-Id", session.session_uuid)]
ok_data = {
"category": faker.pystr(20, 50),
"action": faker.pystr(20, 50),
"label": faker.pystr(20, 50),
"eventValue": faker.pystr(20, 50),
"eventTimestamp": str(faker.date_time()),
"pageUrl": faker.pystr(20, 255),
}

url = url_for("analytics.post_user_a_event")
response = client.post(
url,
headers=session_header,
json=ok_data,
)
assert response.status_code == 201, "User event logged."
4 changes: 3 additions & 1 deletion app/session/tests/test_session_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def test_store_session_creation(
assert Sessions.query.count() == 1, "Single session should be created"
created_session = Sessions.query.first()
assert created_session.session_uuid == session_uuid
assert created_session.session_created_timestamp == session_created_timestamp
db_time = created_session.session_created_timestamp.isoformat(" ", "seconds")
expected_time = session_created_timestamp.isoformat(" ", "seconds")
assert db_time == expected_time
assert created_session.user_uuid == user_uuid
assert created_session.ip_address == ip_address

Expand Down
6 changes: 3 additions & 3 deletions app/session/tests/test_session_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_post_session_creates_unique_uuids(client):
assert (
session.session_uuid == response_uuid
), "The endpoint should return same UUID as stored in DB"
assert (
session.session_created_timestamp == faked_now
), "The session object has been created now"
db_time = session.session_created_timestamp.isoformat(" ", "seconds")
expected_time = faked_now.isoformat(" ", "seconds")
assert db_time == expected_time, "The session object has been created now"
assert session.user_uuid == user.user_uuid, "Mocked user linked"
75 changes: 75 additions & 0 deletions app/static/Climate-Mind_bundled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2016,5 +2016,80 @@ paths:
required:
- email
description: Data required to send password reset link
/analytics:
post:
summary: analytics
tags: []
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
message:
type: string
x-stoplight:
id: tzuwqo35n2rgq
examples:
OK:
value:
message: User event logged.
operationId: post-analytics
x-stoplight:
id: stuyme02xllm1
parameters:
- schema:
type: string
in: header
name: X-Session-Id
description: Session uuid
description: Logs a user A event in the analytics_data table for analytics tracking.
requestBody:
content:
application/json:
schema:
type: object
properties:
category:
type: string
x-stoplight:
id: pa1jncup2zz7h
action:
type: string
x-stoplight:
id: aweqa3xo9a1z8
label:
type: string
x-stoplight:
id: 8o1e1k2qqqh0y
eventValue:
type: string
x-stoplight:
id: 1ifn6lz1grqpr
eventTimestamp:
type: string
x-stoplight:
id: ifs0snoziugak
format: date-time
pattern: '%Y-%m-%d %H:%M:%S'
pageUrl:
type: string
x-stoplight:
id: y9xlkarog8mdj
examples:
Example 1:
value:
category: landing_page - webapp
action: get_started
label: session_id
eventValue: 4a3d330f-0a87-4e35-a968-bc4218a27dae
eventTimestamp: '2020-11-18 07:07:19'
pageUrl: 'https://app.climatemind.org/'
description: |-
Required fields and information for the analytics event.
eventTimestamp MUST look like the following string format:
%Y-%m-%d %H:%M:%S (ex: 2020-11-18 07:07:19 )
components:
schemas: {}

0 comments on commit 6a807ba

Please sign in to comment.