Skip to content

Commit 9486a8b

Browse files
committed
feat: Optionally store analytics data
The analytics data is just stored on disk but not made available or processed in any way yet.
1 parent dc5530b commit 9486a8b

File tree

8 files changed

+87
-17
lines changed

8 files changed

+87
-17
lines changed

bbblb/services/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ def configure_logging(config: BBBLBConfig):
215215
async def bootstrap(
216216
config: BBBLBConfig, autostart=True, logging=True
217217
) -> ServiceRegistry:
218-
import bbblb.settings
219218
import bbblb.services.poller
220219
import bbblb.services.recording
220+
import bbblb.services.analytics
221221
import bbblb.services.locks
222222
import bbblb.services.db
223223
import bbblb.services.bbb
@@ -255,6 +255,10 @@ def watch_debug_level(name, old, new):
255255
"importer",
256256
bbblb.services.recording.RecordingManager(config),
257257
)
258+
ctx.register(
259+
"analytics",
260+
bbblb.services.analytics.AnalyticsHandler(config),
261+
)
258262

259263
if autostart:
260264
for name, service in ctx.services.items():

bbblb/services/analytics.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
import json
3+
import logging
4+
5+
from bbblb import model
6+
from bbblb.services import ManagedService
7+
from bbblb.settings import BBBLBConfig
8+
9+
LOG = logging.getLogger(__name__)
10+
11+
12+
class AnalyticsHandler(ManagedService):
13+
"""Store analytics data to {PATH_DATA}/{tenant}/{meetingID}.json"""
14+
15+
def __init__(self, config: BBBLBConfig):
16+
self.store_path = (config.PATH_DATA / "analytics").resolve()
17+
18+
async def on_start(self):
19+
await super().on_start()
20+
21+
async def on_shutdown(self):
22+
return await super().on_shutdown()
23+
24+
async def store(self, tenant: model.Tenant, data: dict):
25+
"""Store the payload of an analytics callback"""
26+
internal_meeting_id = data["internal_meeting_id"]
27+
dst = self.store_path / tenant.name / f"{internal_meeting_id}.json"
28+
await asyncio.to_thread(dst.parent.mkdir, parents=True, exist_ok=True)
29+
await asyncio.to_thread(dst.write_text, json.dumps(data))

bbblb/services/bbb.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ async def trigger_callback(
5252
continue
5353

5454
async def fire_callback(self, callback: model.Callback, payload: dict, clear=True):
55-
url = callback.forward
56-
key = callback.tenant.secret
57-
data = {"signed_parameters": jwt.encode(payload, key, "HS256")}
58-
await self.trigger_callback("POST", url, data=data)
55+
forward_url = callback.forward
56+
if forward_url:
57+
key = callback.tenant.secret
58+
data = {"signed_parameters": jwt.encode(payload, key, "HS256")}
59+
await self.trigger_callback("POST", forward_url, data=data)
5960
if clear:
6061
async with self.db.session() as session, session.begin():
6162
await session.delete(callback)

bbblb/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ class BBBLBConfig(BaseConfig):
235235
#: by LOAD_PENALTY. The applied penalty decreases linearly over time.
236236
LOAD_COOLDOWN: float = 30
237237

238+
#: If true, BBBLB will intercept the analytics-callback-url webhook
239+
#: and dump json files into the {PATH_DATA}/analytics/{tenant}/
240+
#: folder for later analysis (WIP). The callback is only fired if
241+
#: BBB is configured with defaultKeepEvents=true in bbb-web.properties.
242+
ANALYTICS_STORE: bool = False
243+
238244
#: Maximum number of meetings or recordings to return from APIs that
239245
#: potentially return an unlimited amount of data.
240246
MAX_ITEMS: int = 1000

bbblb/web/bbbapi.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -254,20 +254,27 @@ async def _intercept_callbacks(
254254
# For all other known callbacks (other than meta_endCallbackUrl) we assume
255255
# that they follow the JWT model and can be proxied immediately. They still
256256
# need to be intercepted because we have to re-sign their payload.
257-
for meta in ("meta_analytics-callback-url",):
257+
258+
forward = set(("meta_analytics-callback-url",))
259+
intercept = set()
260+
if cxt.config.ANALYTICS_STORE:
261+
intercept.add("meta_analytics-callback-url")
262+
263+
for meta in forward:
258264
orig_url = params.pop(meta, None)
259-
if orig_url:
260-
typename = meta[5:-14] # Just the middle part
261-
if is_new:
262-
callbacks.append(
263-
model.Callback(
264-
uuid=meeting.uuid,
265-
type=typename,
266-
tenant=meeting.tenant,
267-
server=meeting.server,
268-
forward=orig_url,
269-
)
265+
typename = meta[5:-14] # Just the middle part
266+
if is_new:
267+
callbacks.append(
268+
model.Callback(
269+
uuid=meeting.uuid,
270+
type=typename,
271+
tenant=meeting.tenant,
272+
server=meeting.server,
273+
forward=orig_url, # can be None
270274
)
275+
)
276+
277+
if orig_url or meta in intercept:
271278
url = cxt.request.url_for(
272279
"bbblb:callback_proxy",
273280
uuid=meeting.uuid,

bbblb/web/bbblbapi.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import jwt
88

9+
from bbblb.services.analytics import AnalyticsHandler
910
from bbblb.web import bbbapi
1011
from bbblb import model
1112

@@ -257,6 +258,7 @@ async def handle_callback_proxy(ctx: BBBLBApiRequest):
257258
except (UnicodeDecodeError, KeyError, IndexError):
258259
return Response("Invalid request", 400)
259260

261+
# Forward callbacks requested by the original client
260262
stmt = model.Callback.select(uuid=meeting_uuid, type=callback_type)
261263
callbacks = (await ctx.session.execute(stmt)).scalars().all()
262264
if not callbacks:
@@ -269,8 +271,15 @@ async def handle_callback_proxy(ctx: BBBLBApiRequest):
269271
except BaseException:
270272
return Response("Access denied, signature check failed", 401)
271273

274+
# Intercept callbacks we are interested in
275+
if ctx.config.ANALYTICS_STORE and callback_type == "analytics":
276+
analytics = await ctx.services.use("analytics", AnalyticsHandler)
277+
# Fire and forget
278+
asyncio.create_task(analytics.store(callbacks[0].tenant, payload))
279+
272280
# Find and trigger callbacks
273281
for callback in callbacks:
282+
# Fire and forget
274283
asyncio.create_task(ctx.bbb.fire_callback(callback, payload, clear=True))
275284

276285
return Response("OK", 200)

docs/_config.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ meeting age. See LOAD_COOLDOWN.
103103
Number of minutes after which new meetings are no longer impacted
104104
by LOAD_PENALTY. The applied penalty decreases linearly over time.
105105

106+
``ANALYTICS_STORE`` (type: ``bool``, default: ``False``)
107+
108+
If true, BBBLB will intercept the analytics-callback-url webhook
109+
and dump json files into the {PATH_DATA}/analytics/{tenant}/
110+
folder for later analysis (WIP). The callback is only fired if
111+
BBB is configured with defaultKeepEvents=true in bbb-web.properties.
112+
106113
``MAX_ITEMS`` (type: ``int``, default: ``1000``)
107114

108115
Maximum number of meetings or recordings to return from APIs that

examples/bbblb-compose/bbblb.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@
103103
# (default: 30; type: float)
104104
#BBBLB_LOAD_COOLDOWN=
105105

106+
# If true, BBBLB will intercept the analytics-callback-url webhook
107+
# and dump json files into the {PATH_DATA}/analytics/{tenant}/
108+
# folder for later analysis (WIP). The callback is only fired if
109+
# BBB is configured with defaultKeepEvents=true in bbb-web.properties.
110+
# (default: False; type: bool)
111+
#BBBLB_ANALYTICS_STORE=
112+
106113
# Maximum number of meetings or recordings to return from APIs that
107114
# potentially return an unlimited amount of data.
108115
# (default: 1000; type: int)

0 commit comments

Comments
 (0)