[{"data":1,"prerenderedAt":819},["ShallowReactive",2],{"navigation":3,"\u002Freference\u002Factivity-tracking":189,"\u002Freference\u002Factivity-tracking-surround":814},[4,34,60,116,137,153],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":33},"Getting Started","i-lucide-rocket","\u002Fgetting-started","1.getting-started",[10,15,18,23,28],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-house",{"title":5,"path":16,"stem":17,"icon":6},"\u002Fgetting-started\u002Fgetting-started","1.getting-started\u002F2.getting-started",{"title":19,"path":20,"stem":21,"icon":22},"Choosing a Preset","\u002Fgetting-started\u002Fchoosing-a-preset","1.getting-started\u002F3.choosing-a-preset","i-lucide-git-branch",{"title":24,"path":25,"stem":26,"icon":27},"Deployment","\u002Fgetting-started\u002Fdeployment","1.getting-started\u002F4.deployment","i-lucide-cloud",{"title":29,"path":30,"stem":31,"icon":32},"Background Maintenance","\u002Fgetting-started\u002Fbackground-maintenance","1.getting-started\u002F5.background-maintenance","i-lucide-timer-reset",false,{"title":35,"icon":36,"path":37,"stem":38,"children":39,"page":33},"Build","i-lucide-wrench","\u002Fbuild","2.build",[40,45,50,55],{"title":41,"path":42,"stem":43,"icon":44},"Routers & Prefixes","\u002Fbuild\u002Frouters-and-prefixes","2.build\u002F1.routers-and-prefixes","i-lucide-route",{"title":46,"path":47,"stem":48,"icon":49},"Configuration","\u002Fbuild\u002Fconfiguration","2.build\u002F2.configuration","i-lucide-settings",{"title":51,"path":52,"stem":53,"icon":54},"Authorization Dependencies","\u002Fbuild\u002Fauthorization-dependencies","2.build\u002F3.authorization-dependencies","i-lucide-shield-check",{"title":56,"path":57,"stem":58,"icon":59},"Command Line","\u002Fbuild\u002Fcli","2.build\u002F4.cli","i-lucide-terminal",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":33},"Auth","i-lucide-lock","\u002Fauth","3.auth",[66,71,76,81,86,91,96,101,106,111],{"title":67,"path":68,"stem":69,"icon":70},"OAuth & Social Login","\u002Fauth\u002Foauth-and-social-login","3.auth\u002F1.oauth-and-social-login","i-lucide-log-in",{"title":72,"path":73,"stem":74,"icon":75},"Multi-Frontend Support","\u002Fauth\u002Fmulti-frontend","3.auth\u002F10.multi-frontend","i-lucide-layout-grid",{"title":77,"path":78,"stem":79,"icon":80},"Sessions & Audit","\u002Fauth\u002Fsessions-and-audit","3.auth\u002F2.sessions-and-audit","i-lucide-monitor-smartphone",{"title":82,"path":83,"stem":84,"icon":85},"Passwordless & Messaging","\u002Fauth\u002Fpasswordless-and-messaging","3.auth\u002F3.passwordless-and-messaging","i-lucide-mail",{"title":87,"path":88,"stem":89,"icon":90},"JWT Tokens","\u002Fauth\u002Fjwt-tokens","3.auth\u002F4.jwt-tokens","i-lucide-key-round",{"title":92,"path":93,"stem":94,"icon":95},"User Management API","\u002Fauth\u002Fuser-management-api","3.auth\u002F5.user-management-api","i-lucide-users",{"title":97,"path":98,"stem":99,"icon":100},"User Invitations","\u002Fauth\u002Fuser-invitations","3.auth\u002F6.user-invitations","i-lucide-send",{"title":102,"path":103,"stem":104,"icon":105},"Roles & Permissions","\u002Fauth\u002Froles-and-permissions","3.auth\u002F7.roles-and-permissions","i-lucide-shield",{"title":107,"path":108,"stem":109,"icon":110},"ABAC","\u002Fauth\u002Fabac","3.auth\u002F8.abac","i-lucide-filter",{"title":112,"path":113,"stem":114,"icon":115},"User Status","\u002Fauth\u002Fuser-status","3.auth\u002F9.user-status","i-lucide-user-cog",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":33},"Enterprise","i-lucide-building-2","\u002Fenterprise","4.enterprise",[122,127,132],{"title":123,"path":124,"stem":125,"icon":126},"Core Authorization Concepts","\u002Fenterprise\u002Fcore-authorization-concepts","4.enterprise\u002F1.core-authorization-concepts","i-lucide-network",{"title":128,"path":129,"stem":130,"icon":131},"Entities","\u002Fenterprise\u002Fentities","4.enterprise\u002F2.entities","i-lucide-folder-tree",{"title":133,"path":134,"stem":135,"icon":136},"Entity Memberships","\u002Fenterprise\u002Fentity-memberships","4.enterprise\u002F3.entity-memberships","i-lucide-user-plus",{"title":138,"icon":139,"path":140,"stem":141,"children":142,"page":33},"Integrations","i-lucide-plug","\u002Fintegrations","5.integrations",[143,148],{"title":144,"path":145,"stem":146,"icon":147},"API Keys","\u002Fintegrations\u002Fapi-keys","5.integrations\u002F1.api-keys","i-lucide-key",{"title":149,"path":150,"stem":151,"icon":152},"OutlabsAuth UI","\u002Fintegrations\u002Foutlabsauth-ui","5.integrations\u002F2.outlabsauth-ui","i-lucide-layout-dashboard",{"title":154,"icon":155,"path":156,"stem":157,"children":158,"page":33},"Reference","i-lucide-book-marked","\u002Freference","6.reference",[159,164,169,174,179,184],{"title":160,"path":161,"stem":162,"icon":163},"Data Models","\u002Freference\u002Fdata-models","6.reference\u002F1.data-models","i-lucide-database",{"title":165,"path":166,"stem":167,"icon":168},"Activity Tracking","\u002Freference\u002Factivity-tracking","6.reference\u002F2.activity-tracking","i-lucide-activity",{"title":170,"path":171,"stem":172,"icon":173},"Testing","\u002Freference\u002Ftesting","6.reference\u002F3.testing","i-lucide-flask-conical",{"title":175,"path":176,"stem":177,"icon":178},"Observability","\u002Freference\u002Fobservability","6.reference\u002F4.observability","i-lucide-eye",{"title":180,"path":181,"stem":182,"icon":183},"Metrics Reference","\u002Freference\u002Fmetrics-reference","6.reference\u002F5.metrics-reference","i-lucide-chart-bar",{"title":185,"path":186,"stem":187,"icon":188},"Log Events Reference","\u002Freference\u002Flog-events-reference","6.reference\u002F6.log-events-reference","i-lucide-scroll-text",{"id":190,"title":165,"body":191,"description":807,"extension":808,"links":809,"meta":810,"navigation":811,"path":166,"seo":812,"stem":167,"__hash__":813},"docs\u002F6.reference\u002F2.activity-tracking.md",{"type":192,"value":193,"toc":795},"minimark",[194,212,224,227,232,282,288,295,301,303,307,330,377,380,382,386,409,424,430,432,436,531,533,537,548,553,612,615,619,719,725,734,736,740,761,763,767,791],[195,196,197],"note",{},[198,199,200,201,211],"p",{},"— engagement counters for host apps. Design history: DD-049 in ",[202,203,207],"a",{"href":204,"rel":205},"https:\u002F\u002Fgithub.com\u002Foutlabsio\u002FoutlabsAuth\u002Fblob\u002Fmain\u002Fdocs\u002FDESIGN_DECISIONS.md",[206],"nofollow",[208,209,210],"code",{},"docs\u002FDESIGN_DECISIONS.md",".",[198,213,214,215,219,220,223],{},"Optional Redis-backed unique-user sets so dashboards can show ",[216,217,218],"strong",{},"calendar"," DAU \u002F\nMAU \u002F QAU without treating ",[208,221,222],{},"User.last_login"," as “active today.”",[225,226],"hr",{},[228,229,231],"h2",{"id":230},"what-you-get","What you get",[233,234,235,248],"table",{},[236,237,238],"thead",{},[239,240,241,245],"tr",{},[242,243,244],"th",{},"Metric",[242,246,247],{},"Meaning",[249,250,251,262,272],"tbody",{},[239,252,253,259],{},[254,255,256],"td",{},[216,257,258],{},"DAU",[254,260,261],{},"Unique users active on a calendar day (UTC)",[239,263,264,269],{},[254,265,266],{},[216,267,268],{},"MAU",[254,270,271],{},"Unique users active in a calendar month",[239,273,274,279],{},[254,275,276],{},[216,277,278],{},"QAU",[254,280,281],{},"Unique users active in a calendar quarter",[198,283,284,287],{},[216,285,286],{},"WAU is not implemented"," (no weekly keys or queries).",[198,289,290,291,294],{},"These are ",[216,292,293],{},"calendar periods",", not rolling windows: MAU is “active in January,”\nnot “active in the last 30 days.”",[198,296,297,300],{},[208,298,299],{},"last_login"," alone undercounts: long-lived refresh tokens mean users stay active\nfor days without a fresh login. Activity tracking records presence on\nauthenticated traffic instead.",[225,302],{},[228,304,306],{"id":305},"requirements","Requirements",[308,309,310,319],"ul",{},[311,312,313,318],"li",{},[216,314,315],{},[208,316,317],{},"redis_url"," — required when tracking is on (Redis Sets)",[311,320,321,326,327],{},[216,322,323],{},[208,324,325],{},"enable_activity_tracking=True"," — default is ",[208,328,329],{},"False",[331,332,337],"pre",{"className":333,"code":334,"language":335,"meta":336,"style":336},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","auth = EnterpriseRBAC(  # or SimpleRBAC \u002F OutlabsAuth\n    database_url=...,\n    secret_key=...,\n    redis_url=\"redis:\u002F\u002Flocalhost:6379\u002F0\",\n    enable_activity_tracking=True,\n)\n","python","",[208,338,339,347,353,359,365,371],{"__ignoreMap":336},[340,341,344],"span",{"class":342,"line":343},"line",1,[340,345,346],{},"auth = EnterpriseRBAC(  # or SimpleRBAC \u002F OutlabsAuth\n",[340,348,350],{"class":342,"line":349},2,[340,351,352],{},"    database_url=...,\n",[340,354,356],{"class":342,"line":355},3,[340,357,358],{},"    secret_key=...,\n",[340,360,362],{"class":342,"line":361},4,[340,363,364],{},"    redis_url=\"redis:\u002F\u002Flocalhost:6379\u002F0\",\n",[340,366,368],{"class":342,"line":367},5,[340,369,370],{},"    enable_activity_tracking=True,\n",[340,372,374],{"class":342,"line":373},6,[340,375,376],{},")\n",[198,378,379],{},"If tracking is enabled without Redis, initialization fails with a clear error.\nLeave the flag off when you do not need these metrics.",[225,381],{},[228,383,385],{"id":384},"how-it-works","How it works",[387,388,389,396,399],"ol",{},[311,390,391,392,395],{},"On authenticated requests (and some login paths), the library schedules\n",[208,393,394],{},"activity_tracker.track_activity_detached(user_id)"," — non-blocking.",[311,397,398],{},"Redis Sets hold the current day \u002F month \u002F quarter memberships (O(1) add).",[311,400,401,402,405,406,211],{},"A maintenance sync writes counts into Postgres ",[208,403,404],{},"activity_metrics"," for\nhistory and optionally updates ",[208,407,408],{},"User.last_activity",[198,410,411,412,415,416,419,420,423],{},"Production APIs default to ",[208,413,414],{},"background_job_mode=\"disabled\"",". One external\nmaintenance owner should call ",[208,417,418],{},"run_maintenance_once()"," at the host-selected\ncadence. The 30-minute ",[208,421,422],{},"activity_sync_interval"," applies only to explicitly\nenabled embedded development loops.",[198,425,426,427,429],{},"You rarely talk to Redis yourself for dashboards older than the Redis TTL —\nquery ",[208,428,404],{}," for history; use tracker helpers for “right now.”",[225,431],{},[228,433,435],{"id":434},"configuration-knobs","Configuration knobs",[233,437,438,451],{},[236,439,440],{},[239,441,442,445,448],{},[242,443,444],{},"Setting",[242,446,447],{},"Default",[242,449,450],{},"Purpose",[249,452,453,467,481,499,516],{},[239,454,455,460,464],{},[254,456,457],{},[208,458,459],{},"enable_activity_tracking",[254,461,462],{},[208,463,329],{},[254,465,466],{},"Master switch (needs Redis)",[239,468,469,473,478],{},[254,470,471],{},[208,472,422],{},[254,474,475],{},[208,476,477],{},"1800",[254,479,480],{},"Seconds between Redis → Postgres sync",[239,482,483,488,493],{},[254,484,485],{},[208,486,487],{},"activity_update_user_model",[254,489,490],{},[208,491,492],{},"True",[254,494,495,496,498],{},"Sync ",[208,497,408],{}," in the background job",[239,500,501,506,510],{},[254,502,503],{},[208,504,505],{},"activity_store_user_ids",[254,507,508],{},[208,509,329],{},[254,511,512,515],{},[216,513,514],{},"No effect today"," — passed through but unused",[239,517,518,523,528],{},[254,519,520],{},[208,521,522],{},"activity_ttl_days",[254,524,525],{},[208,526,527],{},"90",[254,529,530],{},"Intended retention; cleanup still uses a hard-coded ~90-day cutoff",[225,532],{},[228,534,536],{"id":535},"querying","Querying",[198,538,539,540,543,544,547],{},"Access the tracker after ",[208,541,542],{},"await auth.initialize()"," via ",[208,545,546],{},"auth.activity_tracker","\n(only when enabled).",[549,550,552],"h3",{"id":551},"live-redis","Live (Redis)",[331,554,556],{"className":333,"code":555,"language":335,"meta":336,"style":336},"from datetime import date\n\ndau = await auth.activity_tracker.get_daily_active_users()\nmau = await auth.activity_tracker.get_monthly_active_users()\nqau = await auth.activity_tracker.get_quarterly_active_users()\n\n# Specific periods\ndau_past = await auth.activity_tracker.get_daily_active_users(date(2025, 1, 15))\nmau_past = await auth.activity_tracker.get_monthly_active_users(2025, 1)\nqau_past = await auth.activity_tracker.get_quarterly_active_users(2025, 1)\n",[208,557,558,563,569,574,579,584,588,594,600,606],{"__ignoreMap":336},[340,559,560],{"class":342,"line":343},[340,561,562],{},"from datetime import date\n",[340,564,565],{"class":342,"line":349},[340,566,568],{"emptyLinePlaceholder":567},true,"\n",[340,570,571],{"class":342,"line":355},[340,572,573],{},"dau = await auth.activity_tracker.get_daily_active_users()\n",[340,575,576],{"class":342,"line":361},[340,577,578],{},"mau = await auth.activity_tracker.get_monthly_active_users()\n",[340,580,581],{"class":342,"line":367},[340,582,583],{},"qau = await auth.activity_tracker.get_quarterly_active_users()\n",[340,585,586],{"class":342,"line":373},[340,587,568],{"emptyLinePlaceholder":567},[340,589,591],{"class":342,"line":590},7,[340,592,593],{},"# Specific periods\n",[340,595,597],{"class":342,"line":596},8,[340,598,599],{},"dau_past = await auth.activity_tracker.get_daily_active_users(date(2025, 1, 15))\n",[340,601,603],{"class":342,"line":602},9,[340,604,605],{},"mau_past = await auth.activity_tracker.get_monthly_active_users(2025, 1)\n",[340,607,609],{"class":342,"line":608},10,[340,610,611],{},"qau_past = await auth.activity_tracker.get_quarterly_active_users(2025, 1)\n",[198,613,614],{},"Redis answers only while keys remain within TTL (roughly days for DAU, months\nfor MAU, longer for QAU). Older periods → Postgres.",[549,616,618],{"id":617},"historical-postgres","Historical (Postgres)",[331,620,622],{"className":333,"code":621,"language":335,"meta":336,"style":336},"from datetime import date, timedelta\nfrom sqlmodel import select\nfrom outlabs_auth.models.sql.activity_metric import ActivityMetric\n\nend = date.today()\nstart = end - timedelta(days=30)\n\nresult = await session.execute(\n    select(ActivityMetric)\n    .where(\n        ActivityMetric.metric_type == \"dau\",\n        ActivityMetric.metric_date >= start,\n        ActivityMetric.metric_date \u003C= end,\n    )\n    .order_by(ActivityMetric.metric_date)\n)\nfor row in result.scalars():\n    print(row.metric_date, row.count)\n",[208,623,624,629,634,639,643,648,653,657,662,667,672,678,684,690,696,702,707,713],{"__ignoreMap":336},[340,625,626],{"class":342,"line":343},[340,627,628],{},"from datetime import date, timedelta\n",[340,630,631],{"class":342,"line":349},[340,632,633],{},"from sqlmodel import select\n",[340,635,636],{"class":342,"line":355},[340,637,638],{},"from outlabs_auth.models.sql.activity_metric import ActivityMetric\n",[340,640,641],{"class":342,"line":361},[340,642,568],{"emptyLinePlaceholder":567},[340,644,645],{"class":342,"line":367},[340,646,647],{},"end = date.today()\n",[340,649,650],{"class":342,"line":373},[340,651,652],{},"start = end - timedelta(days=30)\n",[340,654,655],{"class":342,"line":590},[340,656,568],{"emptyLinePlaceholder":567},[340,658,659],{"class":342,"line":596},[340,660,661],{},"result = await session.execute(\n",[340,663,664],{"class":342,"line":602},[340,665,666],{},"    select(ActivityMetric)\n",[340,668,669],{"class":342,"line":608},[340,670,671],{},"    .where(\n",[340,673,675],{"class":342,"line":674},11,[340,676,677],{},"        ActivityMetric.metric_type == \"dau\",\n",[340,679,681],{"class":342,"line":680},12,[340,682,683],{},"        ActivityMetric.metric_date >= start,\n",[340,685,687],{"class":342,"line":686},13,[340,688,689],{},"        ActivityMetric.metric_date \u003C= end,\n",[340,691,693],{"class":342,"line":692},14,[340,694,695],{},"    )\n",[340,697,699],{"class":342,"line":698},15,[340,700,701],{},"    .order_by(ActivityMetric.metric_date)\n",[340,703,705],{"class":342,"line":704},16,[340,706,376],{},[340,708,710],{"class":342,"line":709},17,[340,711,712],{},"for row in result.scalars():\n",[340,714,716],{"class":342,"line":715},18,[340,717,718],{},"    print(row.metric_date, row.count)\n",[198,720,721,722,211],{},"Monthly rows are keyed on the ",[216,723,724],{},"1st of the month",[198,726,727,728,731,732,211],{},"There is ",[216,729,730],{},"no packaged HTTP router"," for DAU\u002FMAU — expose your own admin\nendpoint that calls the tracker or reads ",[208,733,404],{},[225,735],{},[228,737,739],{"id":738},"ops-notes","Ops notes",[308,741,742,747,755],{},[311,743,744,745,211],{},"Production sync runs through one external maintenance owner; enabling\ntracking does not start a loop while ",[208,746,414],{},[311,748,749,750,752,753,211],{},"Prometheus \u002F structured logs may emit activity-related signals when\nobservability is on — see ",[202,751,175],{"href":176}," and\n",[202,754,180],{"href":181},[311,756,757,758,760],{},"For “last seen” UX, prefer ",[208,759,408],{}," (when sync updates it) over\ninventing your own counter.",[225,762],{},[228,764,766],{"id":765},"related","Related",[308,768,769,773,777,784],{},[311,770,771],{},[202,772,46],{"href":47},[311,774,775],{},[202,776,29],{"href":30},[311,778,779,781,782],{},[202,780,87],{"href":88}," — why refresh lifetime skews ",[208,783,299],{},[311,785,786,790],{},[202,787,789],{"href":204,"rel":788},[206],"Design decision DD-049"," — maintainer depth",[792,793,794],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":336,"searchDepth":343,"depth":349,"links":796},[797,798,799,800,801,805,806],{"id":230,"depth":349,"text":231},{"id":305,"depth":349,"text":306},{"id":384,"depth":349,"text":385},{"id":434,"depth":349,"text":435},{"id":535,"depth":349,"text":536,"children":802},[803,804],{"id":551,"depth":355,"text":552},{"id":617,"depth":355,"text":618},{"id":738,"depth":349,"text":739},{"id":765,"depth":349,"text":766},"DAU \u002F MAU style engagement counters.","md",null,{},{"icon":168},{"title":165,"description":807},"EfXZgoJY14Wt--avxn68hh-5ZgRFwuS82J0S9ixQqlk",[815,817],{"title":160,"path":161,"stem":162,"description":816,"icon":163,"children":-1},"Postgres \u002F SQLModel schema reference.",{"title":170,"path":171,"stem":172,"description":818,"icon":173,"children":-1},"Run and extend library and host tests.",1787472744721]