Skip to content

Commit 5a4cfbc

Browse files
authored
Merge pull request #9 from ConductionNL/wip/build-tile-analytics-2026-07-23
feat(tile-usage-analytics): per-tile click analytics + archive
2 parents 519f5bb + a0d7891 commit 5a4cfbc

22 files changed

Lines changed: 2687 additions & 40 deletions

File tree

appinfo/routes.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@
8686
// dashboard does not exist.
8787
['name' => 'dashboardApi#viewEvent', 'url' => '/api/dashboards/{uuid}/view-event', 'verb' => 'POST',
8888
'requirements' => ['uuid' => '[A-Za-z0-9\-]+']],
89+
90+
// REQ-TANLT-002: record a tile click. Authed users only; the
91+
// controller short-circuits silently when the user has opted out
92+
// or analytics is globally disabled (same reused REQ-ANLT-003/004/005
93+
// gates as the dashboard view-event route above). Returns HTTP 204
94+
// on success, 404 when the placement does not exist. `placementId`
95+
// is constrained to digits so the router never confuses it with a
96+
// literal segment.
97+
['name' => 'tileAnalytics#recordClick', 'url' => '/api/tile-click/{placementId}', 'verb' => 'POST',
98+
'requirements' => ['placementId' => '\d+']],
99+
// REQ-TANLT-003: lets the frontend hook know whether tracking is
100+
// currently active for the calling user, so it can suppress the
101+
// record call without re-implementing the gate logic client-side.
102+
['name' => 'tileAnalytics#config', 'url' => '/api/tile-analytics/config', 'verb' => 'GET'],
103+
89104
// REQ-DASH-026: nested dashboard tree.
90105
['name' => 'dashboardApi#tree', 'url' => '/api/dashboards/tree', 'verb' => 'GET'],
91106
// REQ-DASH-027: slug-chain path resolution. The {path} placeholder
@@ -458,6 +473,18 @@
458473
['name' => 'analytics#dashboardDetail', 'url' => '/api/admin/analytics/dashboards/{uuid}', 'verb' => 'GET',
459474
'requirements' => ['uuid' => '[A-Za-z0-9\-]+']],
460475

476+
// Tile usage-analytics admin endpoints (REQ-TANLT-004..005) — a
477+
// strict downward extension of the dashboard view-analytics admin
478+
// endpoints above. All admin-only via ADR-023 action authorization
479+
// inside the controller. The literal `top` and `export` segments
480+
// and the `by-dashboard` prefix precede any wildcard so the router
481+
// never confuses them.
482+
['name' => 'tileAnalytics#topTiles', 'url' => '/api/admin/analytics/tiles/top', 'verb' => 'GET'],
483+
['name' => 'tileAnalytics#exportCsv', 'url' => '/api/admin/analytics/tiles/export', 'verb' => 'GET'],
484+
['name' => 'tileAnalytics#dashboardBreakdown',
485+
'url' => '/api/admin/analytics/tiles/by-dashboard/{uuid}', 'verb' => 'GET',
486+
'requirements' => ['uuid' => '[A-Za-z0-9\-]+']],
487+
461488
// Background feed-refresh trigger (REQ-FRJ-010). Admin-only via
462489
// runtime `IGroupManager::isAdmin` check inside the controller.
463490
['name' => 'admin#refreshFeedsNow', 'url' => '/api/admin/feeds/refresh-now', 'verb' => 'POST'],

docs/features/tiles.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,42 @@ Custom tiles are user-created shortcut cards that provide quick access to Nextcl
2020
| DELETE | `/api/tiles/{id}` | Delete tile |
2121
| POST | `/api/dashboard/{id}/tile` | Place tile on dashboard |
2222

23+
## Usage analytics
24+
25+
Tile usage analytics is a strict, downward **extension** of the
26+
[dashboard view-analytics](../../openspec/specs/dashboard-view-analytics/spec.md)
27+
capability at the tile/widget-placement grain — it does not introduce
28+
any new privacy machinery, only a finer-grained aggregate table.
29+
30+
- Aggregate-only counts stored in `oc_launchpad_tile_clicks`, one row
31+
per `(placementUuid, clickBucket)` per UTC day. No per-event rows
32+
are ever persisted.
33+
- Unique-actor dedup reuses the SAME salted-daily-hash mechanism
34+
(`sha256(userId || dailySalt)`, cached in `ICache` only) and the
35+
SAME `SaltRotationJob` as dashboard views — no second salt or
36+
rotation job.
37+
- Reuses the SAME `launchpad.analytics_enabled` (global) and
38+
`launchpad.analytics_optout` (per-user) settings. There is no
39+
separate tile-analytics opt-out.
40+
- The existing analytics retention-purge job is extended to also
41+
purge `oc_launchpad_tile_clicks` rows older than
42+
`launchpad.analytics_retention_days` in the same run — no second
43+
purge job.
44+
- The frontend fires a fire-and-forget `POST /api/tile-click/{id}` on
45+
tile activation (click or keyboard Enter), gated by
46+
`GET /api/tile-analytics/config` so tracking is suppressed
47+
client-side when analytics is disabled or the user opted out.
48+
49+
### API Endpoints
50+
51+
| Method | Endpoint | Auth | Description |
52+
|--------|----------|------|-------------|
53+
| POST | `/api/tile-click/{placementId}` | Any authed user | Record a click (always 204; no-op when disabled/opted out) |
54+
| GET | `/api/tile-analytics/config` | Any authed user | Whether tracking is active for the caller |
55+
| GET | `/api/admin/analytics/tiles/top` | Admin | Top-N tiles by click count for a period |
56+
| GET | `/api/admin/analytics/tiles/by-dashboard/{uuid}` | Admin | Per-dashboard tile breakdown |
57+
| GET | `/api/admin/analytics/tiles/export` | Admin | CSV export |
58+
2359
## Screenshot
2460

2561
![Dashboard with Tiles](/screenshots/launchpad-dashboard-overview.png)

lib/BackgroundJob/PurgeViewsJob.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
* window is 365 days; admin override via
99
* `launchpad.analytics_retention_days` is clamped to `[30, 3650]`.
1010
*
11+
* Extended by the tile usage-analytics capability (REQ-TANLT-005) to
12+
* also purge `oc_launchpad_tile_clicks` rows older than the SAME
13+
* cutoff in the same run — no second purge job is introduced, per the
14+
* "reuse, don't reinvent" contract for that capability.
15+
*
1116
* Logging is intentionally aggregate-only: row count + cutoff date
1217
* — never any user-attributable identifiers (REQ-ANLT-009 scenario
1318
* "Purge logs execution"). The job is registered via
@@ -30,6 +35,7 @@
3035
namespace OCA\LaunchPad\BackgroundJob;
3136

3237
use OCA\LaunchPad\Db\DashboardViewMapper;
38+
use OCA\LaunchPad\Db\TileClickMapper;
3339
use OCA\LaunchPad\Service\AnalyticsService;
3440
use OCP\AppFramework\Utility\ITimeFactory;
3541
use OCP\BackgroundJob\TimedJob;
@@ -56,12 +62,18 @@ class PurgeViewsJob extends TimedJob
5662
* context).
5763
* @param DashboardViewMapper $viewMapper Aggregate-row
5864
* mapper.
65+
* @param TileClickMapper $tileClickMapper Tile-click
66+
* aggregate-row
67+
* mapper — reuses
68+
* the same cutoff
69+
* date (REQ-TANLT-005).
5970
* @param LoggerInterface $logger PSR logger.
6071
*/
6172
public function __construct(
6273
ITimeFactory $time,
6374
private readonly AnalyticsService $analyticsService,
6475
private readonly DashboardViewMapper $viewMapper,
76+
private readonly TileClickMapper $tileClickMapper,
6577
private readonly LoggerInterface $logger,
6678
) {
6779
parent::__construct(time: $time);
@@ -70,25 +82,31 @@ public function __construct(
7082

7183
/**
7284
* Run the job — delete every aggregate row strictly older than
73-
* the cutoff date.
85+
* the cutoff date, in BOTH the dashboard-views table and the
86+
* tile-clicks table (REQ-TANLT-005 — same cutoff, same run, no
87+
* second job).
7488
*
7589
* @param mixed $argument Required by the base class; unused.
7690
*
7791
* @return void
7892
*
7993
* @spec openspec/specs/dashboard-view-analytics/spec.md
94+
* @spec openspec/specs/dashboard-view-analytics/spec.md
8095
*/
8196
protected function run($argument): void
8297
{
83-
$cutoff = $this->analyticsService->getPurgeCutoffDate();
84-
$deleted = $this->viewMapper->deleteOlderThan(beforeDate: $cutoff);
98+
$cutoff = $this->analyticsService->getPurgeCutoffDate();
99+
$deletedViews = $this->viewMapper->deleteOlderThan(beforeDate: $cutoff);
100+
$deletedClicks = $this->tileClickMapper->deleteOlderThan(beforeDate: $cutoff);
85101

86102
$this->logger->info(
87-
message: 'launchpad analytics purge: deleted '.$deleted.' rows older than '.$cutoff,
103+
message: 'launchpad analytics purge: deleted '.$deletedViews.' view rows and '
104+
.$deletedClicks.' tile-click rows older than '.$cutoff,
88105
context: [
89-
'rows' => $deleted,
90-
'cutoff' => $cutoff,
91-
'retention' => $this->analyticsService->getRetentionDays(),
106+
'viewRows' => $deletedViews,
107+
'tileRows' => $deletedClicks,
108+
'cutoff' => $cutoff,
109+
'retention' => $this->analyticsService->getRetentionDays(),
92110
]
93111
);
94112
}//end run()

0 commit comments

Comments
 (0)