Skip to content

Challenges during dev

Al Amin edited this page Jul 12, 2026 · 1 revision

Dev Log — 2026-07-13

Backend architecture (Logger, EventRepository, schema migrations, REST API) plus the first React-powered admin screen. Documenting the real bugs hit today — the useful part of a dev log isn't "what I built," it's "what broke and why."

1. dbDelta() silently failing to detect an existing table

Symptom: table created fine on activation, but adding a new column later didn't trigger an ALTER TABLE — looked like the whole upgrade mechanism was broken.

Root cause: missing space in CREATE TABLE {$table}(dbDelta() parses the table name with a regex (|CREATE TABLE ([^ ]*)|) that stops at the first space. No space before ( meant the regex consumed straight through into the column definitions, so dbDelta()'s internal "does this table already exist" check never matched a real table name.

Fix: CREATE TABLE {$table} ( — one space, non-negotiable formatting rule for dbDelta().

Lesson: dbDelta() is not a real SQL parser. Its formatting rules (two spaces before PRIMARY KEY's column list, KEY not INDEX, this space) are regex-driven quirks, not style preferences — get one wrong and it fails silently, not loudly.

2. $wpdb->insert() format-array count mismatch

Symptom: none yet, caught in review — $data had 7 keys, $format had 8 entries.

Root cause: copy-paste drift between the two arrays as fields were added.

Fix: define columns + format specifiers as one const array, derive both $data's expected keys and $format from the same source, so there's exactly one place to update per schema change.

Lesson: anything with two parallel arrays that must stay positionally in sync is a bug waiting to happen. Single source of truth beats "remember to update both."

3. Actor vs. object conflation in the schema

Symptom: no bug yet, but a real design flaw — user_id column meant "who did this" in some events and "who it happened to" in others, with the real actor only ever present inside a free-text message string.

Root cause: didn't separate "who performed the action" from "what was acted on" as two distinct concepts up front.

Fix (in progress): consistently pass get_current_user_id() for the actor slot across every handler — except wp_login/wp_logout, where the hook fires after the session state already changed, so get_current_user_id() is unreliable there and the hook's own $user/$user_id parameter is the correct source of truth instead.

Lesson: "who did it" and "what it happened to" need to be separate, independently queryable columns from day one in an audit log. Retrofitting this is a migration; designing it in from the start is free. Also: know exactly when each hook fires relative to state changes — get_current_user_id() isn't a safe default everywhere.

4. package.json bloated with hundreds of transitive dependencies

Symptom: dependencies block listed ~400 packages (Playwright, Lighthouse, ESLint, ~everything @wordpress/scripts pulls in transitively) instead of the 3 packages actually imported by the code.

Root cause: looks like package-lock.json's flattened resolution tree got pasted into package.json by mistake.

Fix: rm -rf node_modules package-lock.json && npm install after trimming package.json back down to what's actually imported (@wordpress/element, @wordpress/api-fetch, @wordpress/components in dependencies; @wordpress/scripts in devDependencies).

Lesson: the test for "does this belong in dependencies" is simple — does any of my source code have an import/require for it? If not, it's either a transitive dependency (belongs in the lockfile only) or a dev-tool dependency (belongs in devDependencies).

5. Webpack config broke after adding Tailwind

Symptom: Invalid configuration object... has an unknown property 'theme'.

Root cause: tailwind.config.js's content (which legitimately has a theme key) ended up merged into webpack.config.js (which has no such key) — easy mistake with two similarly-named config files sitting at the same root level.

Fix: keep them strictly separate — webpack.config.js only ever touches entry/output/webpack-specific keys; tailwind.config.js only ever touches content/theme/corePlugins/important.

Lesson: when introducing a new tool that also wants a root-level *.config.js, double check which file is open before pasting.

6. Enqueued script path didn't match actual build output

Symptom: blank admin page, no console errors obviously pointing at the cause.

Root cause: custom entry: { admin: ... } in webpack.config.js outputs admin.js/admin.asset.php directly into build/, not into a build/admin/ subdirectory — PHP was looking one level too deep.

Fix: matched the require/enqueue paths to the actual webpack output filenames.

Lesson: after any webpack config change, run ls -la on the actual build output before wiring up the PHP side — confirm the real filenames rather than assuming them from the entry key name.

7. CI: syntax-check step could fail on vendor/'s own fixture files

Root cause: find . -name "*.php" walks vendor/, and some packages (PHPStan, PHPCS) ship deliberately-malformed PHP files used to test their own parsers — php -l against those fails CI for a reason unrelated to my code.

Fix: find . -path ./vendor -prune -o -path ./node_modules -prune -o -name "*.php" -print0 | xargs -0 -n1 php -l.

Lesson: exclude vendor/node_modules from any blanket file-walking step in CI, not just the obvious ones (linting, tests) — anything that globs **/*.php needs this guard.


Running theme across today

A lot of today's bugs share one root shape: two things that need to stay in sync, kept in sync by memory instead of by structure$data/$format arrays, package.json/actual imports, webpack.config.js/tailwind.config.js, build output path/enqueue path. The fix pattern is almost always the same: derive one from the other, or make the mismatch impossible to miss (a guard, a single source of truth) rather than trusting future-me to remember.

Still open going into tomorrow

  • permission_callback on /logs REST route still __return_true — needs current_user_can() gating before this goes further
  • Unit tests for Logger (mocked EventRepository) and EventRepository::query() (real WP_UnitTestCase)
  • RBAC — capability checks, likely a custom capability rather than reusing manage_options directly
  • Admin UI: filters (event type, actor, date range), sorting, search, cursor pagination wired to the id-based cursor already in EventRepository::query()
  • Gutenberg dynamic block + editor SlotFill — not started yet