Create clicks table and tracking #36

Closed
opened 2026-06-08 03:21:26 +00:00 by ldraney · 4 comments
Owner

Type

Feature

Lineage

From spike ldraney/palinks #16 (Keycloak auth integration design).
Prerequisite for ldraney/palinks #40 (canvas view with activity-based card sizing).

Repo

ldraney/palinks

User Story

As a palinks user
I want my link clicks to be recorded
So that usage data can drive the canvas layout sizing and surface my most valuable links

Context

The architecture and auth spike docs define a clicks table for tracking link usage. Originally scoped under the auth-roles story for analytics, this table is now also the data foundation for the canvas layout story — card sizing on the canvas scales proportionally to click count.

Schema from docs/auth.md:

CLICKS {
    bigint id PK
    bigint link_id FK "NOT NULL"
    bigint user_id FK "nullable — anonymous clicks have no user"
    string session_id "anonymous tracking identifier"
    datetime clicked_at "NOT NULL"
}

Key design decisions (from spike):

  • Polymorphic attribution: user_id for authenticated clicks, session_id for anonymous. Both nullable individually, at least one present (model-level validation, not DB constraint).
  • user_id column without FK constraint for now: The users table doesn't exist yet (#32). Add user_id as a plain bigint column; FK constraint added in a later migration when users table lands.
  • Feature flag gating deferred: The click_tracking flag is the target gate, but the feature_flags table (#34) doesn't exist yet either. For now, clicks always record. The flag gate is additive — wired in when #34 ships.

File Targets

Files the agent should modify or create:

  • db/migrate/*_create_clicks.rb — create clicks table: link_id (FK to links), user_id (bigint, nullable, no FK yet), session_id (string, nullable), clicked_at (datetime, not null)
  • app/models/click.rb — belongs_to :link, validates at least one of user_id/session_id present, scope for recent/by_link
  • app/models/link.rb — has_many :clicks, #click_count method (or counter cache), #clicks_since(time) for decay calculations
  • app/controllers/clicks_controller.rb — POST create action, sets session_id from session, redirects to link URL
  • config/routes.rb — add click recording route (e.g. POST /links/:link_id/click)
  • app/views/links/_link.html.erb — link clicks go through the tracking endpoint instead of directly to URL
  • app/views/links/show.html.erb — update the direct link to URL to also route through click tracking endpoint
  • test/models/click_test.rb — model validations
  • test/controllers/clicks_controller_test.rb — recording and redirect tests

Files the agent should NOT touch:

  • app/javascript/controllers/sortable_controller.js — unrelated drag-and-drop
  • app/models/concerns/ — no concerns needed for this scope

Feature Flag

Flag: none
Rationale: The target flag is click_tracking but the feature_flags table (#34) doesn't exist yet. Click recording ships ungated; flag gate added when #34 lands.

Acceptance Criteria

  • Clicking a link card records a click row with link_id and clicked_at
  • Anonymous clicks record session_id from the Rails session
  • After recording, user is redirected to the actual link URL
  • Link#click_count returns total clicks for that link
  • Click recording does not break existing link card UI or navigation
  • user_id column exists but is nullable and has no FK constraint
  • Both index partial and show page route through click tracking

Test Expectations

  • Unit test: Click validates presence of at least one of user_id or session_id
  • Unit test: Click belongs_to :link (required)
  • Unit test: Link#click_count returns correct count
  • Integration test: POST to click endpoint creates a click and redirects to link URL
  • Integration test: click_count increments after recording
  • Run command: bin/rails test

Constraints

  • Do NOT add a FK constraint for user_id — users table doesn't exist yet
  • Use redirect-through pattern (POST /links/:id/click -> redirect to URL), not AJAX, to keep it simple and work without JS
  • No counter_cache column yet — use clicks.count or a method; counter cache is an optimization for later when click volume warrants it
  • Keep the controller minimal — record and redirect, no analytics views in this ticket

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • palinks — project this affects
  • #40 — canvas view depends on this for card sizing data
  • #41 — link groups depends on this transitively
### Type Feature ### Lineage From spike `ldraney/palinks #16` (Keycloak auth integration design). Prerequisite for `ldraney/palinks #40` (canvas view with activity-based card sizing). ### Repo `ldraney/palinks` ### User Story As a palinks user I want my link clicks to be recorded So that usage data can drive the canvas layout sizing and surface my most valuable links ### Context The architecture and auth spike docs define a `clicks` table for tracking link usage. Originally scoped under the auth-roles story for analytics, this table is now also the data foundation for the canvas layout story — card sizing on the canvas scales proportionally to click count. Schema from `docs/auth.md`: ``` CLICKS { bigint id PK bigint link_id FK "NOT NULL" bigint user_id FK "nullable — anonymous clicks have no user" string session_id "anonymous tracking identifier" datetime clicked_at "NOT NULL" } ``` Key design decisions (from spike): - **Polymorphic attribution**: `user_id` for authenticated clicks, `session_id` for anonymous. Both nullable individually, at least one present (model-level validation, not DB constraint). - **user_id column without FK constraint for now**: The `users` table doesn't exist yet (#32). Add `user_id` as a plain bigint column; FK constraint added in a later migration when users table lands. - **Feature flag gating deferred**: The `click_tracking` flag is the target gate, but the `feature_flags` table (#34) doesn't exist yet either. For now, clicks always record. The flag gate is additive — wired in when #34 ships. ### File Targets Files the agent should modify or create: - `db/migrate/*_create_clicks.rb` — create clicks table: link_id (FK to links), user_id (bigint, nullable, no FK yet), session_id (string, nullable), clicked_at (datetime, not null) - `app/models/click.rb` — belongs_to :link, validates at least one of user_id/session_id present, scope for recent/by_link - `app/models/link.rb` — has_many :clicks, `#click_count` method (or counter cache), `#clicks_since(time)` for decay calculations - `app/controllers/clicks_controller.rb` — POST create action, sets session_id from session, redirects to link URL - `config/routes.rb` — add click recording route (e.g. `POST /links/:link_id/click`) - `app/views/links/_link.html.erb` — link clicks go through the tracking endpoint instead of directly to URL - `app/views/links/show.html.erb` — update the direct link to URL to also route through click tracking endpoint - `test/models/click_test.rb` — model validations - `test/controllers/clicks_controller_test.rb` — recording and redirect tests Files the agent should NOT touch: - `app/javascript/controllers/sortable_controller.js` — unrelated drag-and-drop - `app/models/concerns/` — no concerns needed for this scope ### Feature Flag Flag: none Rationale: The target flag is `click_tracking` but the feature_flags table (#34) doesn't exist yet. Click recording ships ungated; flag gate added when #34 lands. ### Acceptance Criteria - [ ] Clicking a link card records a click row with link_id and clicked_at - [ ] Anonymous clicks record session_id from the Rails session - [ ] After recording, user is redirected to the actual link URL - [ ] `Link#click_count` returns total clicks for that link - [ ] Click recording does not break existing link card UI or navigation - [ ] user_id column exists but is nullable and has no FK constraint - [ ] Both index partial and show page route through click tracking ### Test Expectations - [ ] Unit test: Click validates presence of at least one of user_id or session_id - [ ] Unit test: Click belongs_to :link (required) - [ ] Unit test: Link#click_count returns correct count - [ ] Integration test: POST to click endpoint creates a click and redirects to link URL - [ ] Integration test: click_count increments after recording - Run command: `bin/rails test` ### Constraints - Do NOT add a FK constraint for user_id — users table doesn't exist yet - Use redirect-through pattern (POST /links/:id/click -> redirect to URL), not AJAX, to keep it simple and work without JS - No counter_cache column yet — use `clicks.count` or a method; counter cache is an optimization for later when click volume warrants it - Keep the controller minimal — record and redirect, no analytics views in this ticket ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `palinks` — project this affects - `#40` — canvas view depends on this for card sizing data - `#41` — link groups depends on this transitively
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-1400-2026-06-09
Spec is thorough and well-structured -- all template sections present, file targets verified, acceptance criteria testable. Two items need attention before READY:

  • [BODY] app/views/links/show.html.erb line 4 also links directly to link.url but is not listed in File Targets. Add it to the modify list (for click tracking consistency) or to the "should NOT touch" list with rationale.
  • [SCOPE] No arch-palinks architecture note exists in pal-e-docs. This is a cross-cutting gap affecting all palinks board items -- create the note to complete the traceability triangle.
## Scope Review: NEEDS_REFINEMENT Review note: `review-1400-2026-06-09` Spec is thorough and well-structured -- all template sections present, file targets verified, acceptance criteria testable. Two items need attention before READY: - **[BODY]** `app/views/links/show.html.erb` line 4 also links directly to `link.url` but is not listed in File Targets. Add it to the modify list (for click tracking consistency) or to the "should NOT touch" list with rationale. - **[SCOPE]** No `arch-palinks` architecture note exists in pal-e-docs. This is a cross-cutting gap affecting all palinks board items -- create the note to complete the traceability triangle.
Author
Owner

Scope refinement (2026-06-09):

Issue body populated from spike docs (docs/auth.md, docs/architecture.md) and updated after first scope review.

Changes from review feedback:

  • Added app/views/links/show.html.erb to file targets — the show page also links directly to link.url and needs to route through click tracking
  • Added acceptance criterion: "Both index partial and show page route through click tracking"

Reviewer also flagged missing arch-palinks note in pal-e-docs — that's a cross-cutting gap across all palinks board items, not specific to this ticket. Tracked separately.

**Scope refinement (2026-06-09):** Issue body populated from spike docs (`docs/auth.md`, `docs/architecture.md`) and updated after first scope review. Changes from review feedback: - Added `app/views/links/show.html.erb` to file targets — the show page also links directly to `link.url` and needs to route through click tracking - Added acceptance criterion: "Both index partial and show page route through click tracking" Reviewer also flagged missing `arch-palinks` note in pal-e-docs — that's a cross-cutting gap across all palinks board items, not specific to this ticket. Tracked separately.
Author
Owner

Scope Review: APPROVED

Review note: review-1400-2026-06-09-rerev

Re-review after refinement. Both items from the previous NEEDS_REFINEMENT review are resolved:

  • show.html.erb now included in File Targets with clear modification intent, plus new AC #7 ("Both index partial and show page route through click tracking")
  • arch-palinks note acknowledged as board-wide gap, not blocking individual tickets

All 9 file targets verified against codebase. All 7 acceptance criteria are agent-verifiable. Standard Rails resource pattern -- no decomposition needed. Ready for next_up.

## Scope Review: APPROVED Review note: `review-1400-2026-06-09-rerev` Re-review after refinement. Both items from the previous NEEDS_REFINEMENT review are resolved: - **show.html.erb** now included in File Targets with clear modification intent, plus new AC #7 ("Both index partial and show page route through click tracking") - **arch-palinks note** acknowledged as board-wide gap, not blocking individual tickets All 9 file targets verified against codebase. All 7 acceptance criteria are agent-verifiable. Standard Rails resource pattern -- no decomposition needed. Ready for next_up.
Author
Owner

Validation: PASS

Tiers executed: Tier 1 (local tests), Tier 3 (production)
Validation note: validation-36-2026-06-13

7 checks: 7 PASS, 0 FAIL

# Criterion Result
1 Click card records click row with link_id and clicked_at PASS
2 Anonymous clicks record session_id from Rails session PASS
3 After recording, user is redirected to actual link URL PASS
4 Link#click_count returns total clicks PASS
5 Click recording does not break existing UI/navigation PASS
6 user_id column nullable, no FK constraint PASS
7 Both index partial and show page route through click tracking PASS

Evidence:

  • Woodpecker pipeline #19: success
  • Pod palinks-6b8fbf884f-qb5ss running image e5352d7f..., 0 restarts
  • POST /links/6/click returns 302, redirects to correct URL
  • Click count incremented from 0 to 1 on show page
  • All routes healthy (/, /links, /links/new, /links/:id, /links/:id/edit, /up)
  • Local tests: 14 runs, 28 assertions, 0 failures
## Validation: PASS Tiers executed: Tier 1 (local tests), Tier 3 (production) Validation note: `validation-36-2026-06-13` **7 checks: 7 PASS, 0 FAIL** | # | Criterion | Result | |---|-----------|--------| | 1 | Click card records click row with link_id and clicked_at | PASS | | 2 | Anonymous clicks record session_id from Rails session | PASS | | 3 | After recording, user is redirected to actual link URL | PASS | | 4 | Link#click_count returns total clicks | PASS | | 5 | Click recording does not break existing UI/navigation | PASS | | 6 | user_id column nullable, no FK constraint | PASS | | 7 | Both index partial and show page route through click tracking | PASS | **Evidence:** - Woodpecker pipeline #19: success - Pod `palinks-6b8fbf884f-qb5ss` running image `e5352d7f...`, 0 restarts - POST `/links/6/click` returns 302, redirects to correct URL - Click count incremented from 0 to 1 on show page - All routes healthy (/, /links, /links/new, /links/:id, /links/:id/edit, /up) - Local tests: 14 runs, 28 assertions, 0 failures
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/palinks#36
No description provided.