55from pathlib import Path
66
77# ---------------------------------------------------------------------------
8- # Logging — configure first so every subsequent import sees the right level
8+ # Logging — configure first
99# ---------------------------------------------------------------------------
1010
11-
12- class _UserIdFilter (logging .Filter ):
13- """Injects ``user_id`` into every log record."""
14-
15- def filter (self , record : logging .LogRecord ) -> bool :
16- try :
17- from flask import g as flask_g
18- from flask import has_request_context
19-
20- record .user_id = flask_g .get ("uid" , 0 ) if has_request_context () else 0
21- except Exception :
22- record .user_id = "?"
23- return True
24-
25-
26- _handler = logging .StreamHandler ()
27- _handler .addFilter (_UserIdFilter ())
28- _handler .setFormatter (logging .Formatter ("[%(asctime)s] %(levelname)s uid=%(user_id)s %(name)s: %(message)s" ))
29- logging .root .addHandler (_handler )
30- logging .root .setLevel (logging .INFO )
11+ logging .basicConfig (
12+ level = logging .INFO ,
13+ format = "[%(asctime)s] %(levelname)s %(name)s: %(message)s" ,
14+ )
3115
3216_access_log = logging .getLogger ("tracekit.access" )
3317
3418# ---------------------------------------------------------------------------
35- # Sentry — initialise after logging so its handler inherits the INFO level
19+ # Sentry — modern SDK 2.x setup (Flask + Gunicorn safe)
3620# ---------------------------------------------------------------------------
3721
3822if _sentry_dsn := os .environ .get ("SENTRY_DSN" ):
3923 import sentry_sdk
24+ from sentry_sdk .integrations .flask import FlaskIntegration
4025
4126 def _traces_sampler (sampling_context : dict ) -> float :
42- request = sampling_context .get ("request" ) or {}
43- url = request .get ("url" , "" )
44-
45- if url .endswith ("/health" ):
27+ """
28+ Drop healthcheck traces.
29+ Sample everything else at 100%.
30+ """
31+ tx_ctx = sampling_context .get ("transaction_context" ) or {}
32+ name = tx_ctx .get ("name" )
33+
34+ if name == "api.health" :
4635 return 0.0
4736
4837 return 1.0
4938
5039 sentry_sdk .init (
5140 dsn = _sentry_dsn ,
52- send_default_pii = True ,
41+ environment = os .getenv ("SENTRY_ENV" , "production" ),
42+ # Performance
5343 traces_sampler = _traces_sampler ,
54- enable_logs = True ,
55- profile_session_sample_rate = 1.0 ,
5644 profile_lifecycle = "trace" ,
45+ profile_session_sample_rate = 1.0 ,
46+ # Logs (new 2.x logs product)
47+ enable_logs = True ,
48+ # Flask integration
49+ integrations = [FlaskIntegration ()],
50+ # Attach user + request info automatically
51+ send_default_pii = True ,
5752 )
5853
5954# ---------------------------------------------------------------------------
6055# Re-exports kept for backward-compatibility with the test suite
6156# ---------------------------------------------------------------------------
57+
6258from calendar_data import get_sync_calendar_data # noqa: F401
6359from db_init import (
64- _init_db , # noqa: F401
60+ _init_db ,
6561 load_tracekit_config ,
6662)
6763from flask import Flask , abort , g , redirect , request , session , url_for
@@ -85,7 +81,7 @@ def _traces_sampler(sampling_context: dict) -> float:
8581app .secret_key = os .environ ["SESSION_KEY" ]
8682
8783# ---------------------------------------------------------------------------
88- # Template context — inject current_user into every template
84+ # User context + DB initialization
8985# ---------------------------------------------------------------------------
9086
9187
@@ -95,21 +91,11 @@ def _set_user_context():
9591
9692 from tracekit .user_context import set_user_id
9793
98- # Initialise g.uid so the log filter always has a value for this request,
99- # even if we return early or abort below.
100- g .uid = 0
101-
10294 if is_single_user_mode ():
10395 set_user_id (0 )
10496 return
10597
106- # Ensure the DB is initialised and connected before any route that may query
107- # it — including unauthenticated routes like /login and /signup. This
108- # matters for fresh Gunicorn worker processes where _db_initialized is still
109- # False. If the DB is genuinely unavailable, abort with 503.
11098 try :
111- from db_init import _init_db
112-
11399 from tracekit .db import get_db
114100
115101 _init_db ()
@@ -119,9 +105,6 @@ def _set_user_context():
119105
120106 uid = session .get ("user_id" )
121107 if not uid :
122- # Unauthenticated request — explicitly reset to 0. ContextVars are not
123- # reset between requests in a single-threaded WSGI process, so without
124- # this a previous authenticated request's user_id would bleed through.
125108 set_user_id (0 )
126109 return
127110
@@ -131,35 +114,33 @@ def _set_user_context():
131114
132115 user = User .get_by_id (uid )
133116 set_user_id (user .id )
134- g .uid = user .id
135- # Cache on g so the context processor never needs to re-query the DB.
136- # Tracekit.cleanup() closes the connection before templates render, so a
137- # second DB round-trip in inject_current_user() would intermittently fail.
138117 g .current_user = user
139118 except Exception as exc :
140119 import peewee
141120
142121 if isinstance (exc , peewee .DoesNotExist ):
143- # The user row was deleted — log out cleanly.
144122 session .pop ("user_id" , None )
145123 else :
146- # Any other DB error: abort rather than writing data under user_id=0.
147124 abort (503 )
148125
149126
150127@app .context_processor
151128def inject_current_user ():
152129 from auth_mode import is_single_user_mode
153130
154- single_user_mode = is_single_user_mode ()
155- if single_user_mode :
131+ if is_single_user_mode ():
156132 return {"current_user" : None , "single_user_mode" : True }
157133
158- # Use the user cached by before_request — no additional DB query needed.
159- current_user = g .get ("current_user" )
160- return {"current_user" : current_user , "single_user_mode" : False }
134+ return {
135+ "current_user" : g .get ("current_user" ),
136+ "single_user_mode" : False ,
137+ }
161138
162139
140+ # ---------------------------------------------------------------------------
141+ # Auth enforcement
142+ # ---------------------------------------------------------------------------
143+
163144_PUBLIC_ENDPOINTS = frozenset (
164145 {
165146 "auth.login" ,
@@ -175,7 +156,6 @@ def inject_current_user():
175156
176157@app .before_request
177158def _require_auth ():
178- """Redirect unauthenticated users to /login in multi-user mode."""
179159 from auth_mode import is_single_user_mode
180160
181161 if is_single_user_mode ():
@@ -184,13 +164,24 @@ def _require_auth():
184164 return
185165 if g .get ("current_user" ):
186166 return
167+
187168 return redirect (url_for ("auth.login" ))
188169
189170
171+ # ---------------------------------------------------------------------------
172+ # Request logging
173+ # ---------------------------------------------------------------------------
174+
175+
190176@app .after_request
191177def _log_request (response ):
192178 if request .endpoint != "api.health" :
193- _access_log .info ("%s %s %s" , request .method , request .path , response .status_code )
179+ _access_log .info (
180+ "%s %s %s" ,
181+ request .method ,
182+ request .path ,
183+ response .status_code ,
184+ )
194185 return response
195186
196187
@@ -221,7 +212,7 @@ def _log_request(response):
221212app .register_blueprint (stripe_bp )
222213
223214# ---------------------------------------------------------------------------
224- # CLI entry point
215+ # CLI entry point (dev only)
225216# ---------------------------------------------------------------------------
226217
227218if __name__ == "__main__" :
@@ -238,8 +229,9 @@ def _log_request(response):
238229 print (" Health: http://localhost:5000/health" )
239230 print ("\n Press Ctrl+C to stop" )
240231
241- try :
242- app .run (debug = config .get ("debug" , False ), host = "0.0.0.0" , port = 5000 , threaded = True )
243- except Exception as e :
244- print (f"Server failed to start: { e } " )
245- exit (1 )
232+ app .run (
233+ debug = config .get ("debug" , False ),
234+ host = "0.0.0.0" ,
235+ port = 5000 ,
236+ threaded = True ,
237+ )
0 commit comments