Skip to content

Feature Bingo Developer Guide

Ed Mozley edited this page Oct 2, 2026 · 4 revisions

Feature Bingo β€” developer guide

Added in 2.8.0 Β· User page: Feature Bingo

One card per feature, with a star that lights when the feature is set up. A card is data, not code, and every new feature should arrive with its card.


Files

File
includes/feature_bingo.php the format (documented at the top), modules, categories, tiers, the loader and validator (featureBingoCards()), the check runner (featureBingoRunCheck()), featureBingoEvaluate()
includes/feature_bingo/cards/*.php the cards that ship - one file per module or area, each return [ ...cards ]
local/feature_bingo/*.php an install's own cards - same format, never shipped or overwritten (below). FEATURE_BINGO_LOCAL_DIR moves it
api/system/feature_bingo.php GET the cards with their state; POST dismiss / restore / dismiss_module / restore_module (admins only; only real card ids)
system/feature-bingo/index.php the page
scripts/feature_bingo_check.php runs every check against a database and reports errors and bad links
feature_bingo_dismissed the only data: cards marked Not for us (card_id primary key - listed in $primaryKeys in api/system/db_verify.php, because it is not id)

A card

[
    'id'       => 'tickets.sla_calendar',        // unique, stable - Not for us is stored against it
    'module'   => 'tickets',                      // featureBingoModules()
    'tier'     => 'recommended',                  // essential | recommended | extra
    'category' => 'governance',                   // featureBingoCategories()
    'title'    => 'Business hours for SLAs',
    'what'     => 'What it is, 1-2 sentences.',
    'why'      => 'Why it is worth it, 1-2 sentences.',
    'done'     => 'Exactly what the check counts, in plain words.',
    'link'     => 'tickets/settings/#sla',       // app-relative; Tickets settings opens #<tab>
    'check'    => ['rows', 'sla_business_calendars'],
]

Checks - declarative and read-only

Check Lights when
['rows', 'table'] / ['rows', 'table', 'where', min] at least min (1) matching rows
['setting', 'key', 'eq'|'neq'|'in'|'nonempty', value] a system_settings value; a never-saved key is never configured
['sql', 'SELECT COUNT(*) ...', min] one number β‰₯ min. One statement, SELECT only
['any', [...]] / ['all', [...]] combinations

featureBingoAssertReadOnly() refuses anything but a single SELECT - judging the SQL with quoted strings blanked out, so WHERE x <> 'delete' is allowed and ; DROP is not. Cards are repository code, so this guards against mistakes, not attackers.

A check that errors reads as not configured and never breaks the page - which is exactly why scripts/feature_bingo_check.php exists: an unknown table or column would otherwise leave a star dark forever with nothing saying why.


βž• Adding your own cards (your install)

Feature Bingo is meant to be extended. Two different cases:

You are... Put the card in Why
contributing to FreeITSM - a new feature for everyone includes/feature_bingo/cards/<module>.php, in the same pull request as the feature it ships with the release
running your own copy - your own processes, or something you added to your install local/feature_bingo/<anything>.php no release ships, overwrites or deletes that folder

Why not just drop a file into includes/feature_bingo/cards/ on your own install? It works until the next update. With git, an extra file survives a pull, but a release that changes that folder can collide with it. In Docker the code is part of the image, so the file is simply gone after the next docker compose pull. local/ is in .gitignore and nothing in FreeITSM writes to it.

Step by step

  1. Create local/feature_bingo/ in the FreeITSM folder (next to includes/), and a file in it - say our-cards.php:
<?php
return [
    [
        'id'       => 'local.ops_team',            // unique across ALL cards - prefix yours with local.
        'module'   => 'system',                     // any key of featureBingoModules()
        'tier'     => 'recommended',                // essential | recommended | extra
        'category' => 'organisation',               // any key of featureBingoCategories()
        'title'    => 'An Operations team',
        'what'     => 'A team called Operations that our out-of-hours rota is built on.',
        'why'      => 'Our escalation policy assumes it exists.',
        'done'     => 'A team named Operations exists.',
        'link'     => 'system/teams/',             // app-relative; must exist
        'check'    => ['rows', 'teams', "name = 'Operations'"],
    ],
];
  1. Check it: php scripts/feature_bingo_check.php - it loads your folder too, and reports a malformed card, a duplicate id, a check that errors (a wrong table or column) or a link to a page that does not exist. Run it every time you add a card: on the page, a broken check just reads as "not set up", forever.
  2. Open System β†’ Feature Bingo. Your cards appear in their module with a small Added here tag, count in the score, and can be marked Not for us like any other.

Somewhere else, or Docker

To keep the folder somewhere else, add to config.php:

define('FEATURE_BINGO_LOCAL_DIR', '/srv/freeitsm-local/feature_bingo');

Docker: put the folder on a volume, or it goes with the container. In docker-compose.yml, under the app's volumes::

      - ./my-bingo-cards:/var/www/html/local/feature_bingo

Writing a good check

Everything under Writing a check that tells the truth below applies - especially seeded rows and pages that save every setting at once. Checks can only read: rows, setting, sql (a single SELECT returning one number), any, all. To check a feature of your own, count the rows or the setting it writes.


πŸ”΄ Writing a check that tells the truth

  • Seeded rows. database/freeitsm.sql and api/system/db_verify.php insert defaults (statuses, priorities, ticket types, origins, resolution codes, checklist roles, dashboard widgets...). Count only rows outside the seed, or a fresh install lights the card.
  • Demo data. Exclude is_demo = 1 wherever the column exists.
  • Settings pages that save everything at once (Tickets β†’ General, CSAT, Branding, Colours, System β†’ Managers) - one Save writes every key. A "was saved" check then lights with its neighbours; compare against the default where "changed" is what matters, and say which in done.
  • Defaults that are ON. A switch that is on unless turned off cannot be told apart from never-visited; those cards light once saved switched on. Say so in done.
  • JSON in columns (workflow actions, form configs) - match the exact shape the editor writes ("type":"send_email", no spaces), and check it against real rows.

Built

2.8.0 shipped 572 cards across 24 modules (Tickets 143, System 86, Workflows 41, Self-service portal 40...), written by module from each settings page, its manifest's setting_keys, the schema and the help. Duplicates were removed mechanically - two cards with an identical check are the same feature - keeping the one in the module where it is used or set up. By 3.0.0 there are 613, across 25 modules (People joined with its own three; LMS gained three for competency tests).

Tested: the validator on two databases (0 malformed, 0 errors, 0 bad links; a deliberately broken card was reported); the whole evaluation in ~0.33 s; Not for us on a card and a module, restored; a made-up card id and a non-admin refused; Tickets β†’ Settings opening #sla / #csat, and an unknown tab falling back.

Known gaps: Docker HTTPS (leaves nothing in the database) and Topology (read-only) have no card; per-analyst preferences are deliberately not cards.


See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally