Goal
You wrote a sibling plugin (or a custom hook) that injects data into the mobile
payload, but the app doesn’t show it. Work through this checklist to find which
link in the chain dropped your data: the filter never ran, the JSON shape is
wrong, the cursor cache served a stale “no change” response, the priority put
you in the wrong order, a primitive type the app rejects, or a stale form cache.
The checklist moves from cheapest to most expensive to check, and from
server-side to client-side. Stop at the first step that reproduces the problem —
the failure is almost always one link, not several.
Prerequisites
- The plugin or hook that should be injecting the data, and the exact key name
you expect to see. - A way to hit the phone-data endpoint directly (a logged-in session cookie, or
the app’s request, captured). The endpoint is thelocal_app_get_phone_data
AJAX action onadmin-ajax.php. - Access to the WordPress install (to inspect options and the
$wp_filter
registry) — a WP-CLI shell or a temporary debug hook is enough. mam-mainactive. It owns the phone-data pipeline, the cursor cache, and the
output shaping every other plugin’s data passes through.
How the payload reaches the app
When the app launches it calls local_app_get_phone_data. mam-main runs that
request through its phone-data pipeline, which assembles the payload in phases
and, near the end, fires:
$filtered = apply_filters( 'mam_get_phone_data_before_send', $context->to_array() );
Every contribution — app settings, user roles, notifications, forms, home_cats,
and your plugin — subscribes to this one filter. Before the pipeline does any of
that expensive work, it consults the cursor cache: if the cursor the app sent
matches the server’s current cursor, nothing has changed and it returns a tiny
no_new_data stub instead of building the payload. Otherwise it runs the filter,
then output shaping (mam_replace_null_with_empty_string()), echoes the current
cursor back onto the response, and returns the JSON.
That gives you five places your data can disappear, which map to the steps
below: the filter, the JSON shape, the cursor cache, the subscriber priority,
and (for forms) the form cache.
Steps
1. Confirm the key is in the raw JSON at all
Before anything else, look at the actual payload the server produces. Hit the
endpoint directly and dump it:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data'
-b 'cookie=...your logged-in session...' > payload.json
Then search payload.json for your key.
- Key present, with the data you expect → the server side is fine. Skip to
step 5 (cursor) and step 7 (client) — the problem is between server and app,
not in your injection. - The whole response is just
{"json_status":"no_new_data","cursor":...}→
the cursor decided nothing changed since the app’s last request and skipped the
build entirely. Go to step 5. - Key present but its value is
nullor a single space → that is anull
your own data produced, not a cache signal; output shaping rewrites anull
field to a single space. Fix the value at the source. - Key absent entirely → your filter either didn’t run, ran too late, or
returned the wrong thing. Continue to step 2.
The endpoint is the contract between site and app. If the key isn’t here, the
app cannot possibly render it — there is no point looking at the app yet.
2. Confirm your subscriber actually runs
The most common cause of an absent key is a filter that never fired, or one that
fired and threw away the array. Two checks:
Is your callback registered? mam_get_phone_data_before_send has dozens of
subscribers. Enumerate them in priority order to confirm yours is in the list:
global $wp_filter;
$hook = $wp_filter['mam_get_phone_data_before_send'] ?? null;
if ( $hook ) {
foreach ( $hook->callbacks as $priority => $callbacks ) {
echo "Priority $priority:n";
foreach ( $callbacks as $cb_id => $def ) {
echo " - $cb_idn";
}
}
}
If your callback isn’t listed, it was never added — check that the plugin is
active, that it loads after mam-main, and that the add_filter call actually
executes (a guard clause or an entitlement check may be returning early in the
constructor before the hook is registered).
Does your callback return the array? The single most destructive bug in a
phone-data subscriber is returning null, false, or a fresh array instead of
the accumulated $data_array. That doesn’t just drop your key — it drops
every subscriber’s contribution that ran before you. Always:
add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {
$data_array['my_plugin_widget'] = $this->build_widget();
return $data_array; // <- never `return;`, never `return true;`
}, 10 );
3. Confirm the JSON shape matches what the app parses
If the key is present but the app still ignores it, the value’s shape may
violate a convention deployed mobile parsers depend on. The mobile JSON contract
is frozen — older parsers in the field are strict about primitive types:
- Ids must be strings, even when numeric. Ship
"123", not123. Post ids,
user ids, button ids, and form indexes are all string-typed in the payload. - Flags must be
"yes"/"no"strings (some places use"on"/"off"),
not booleans. Don’t shiptrue/falsewhere the convention is a string. - Plain integers are fine for non-id numbers — counts, indexes, coordinates.
A field that is 123 where the app expects "123", or true where it expects
"yes", can cause the app to silently skip the whole section rather than render
a malformed value.
One more shape trap: a null field value is rewritten to a single space
(" ") by mam_replace_null_with_empty_string() during output shaping. The
function recurses the whole array, so every null becomes a space at any depth —
you can’t rely on the app distinguishing “null” from “empty”. Ship the value you
actually mean rather than a null you expect to survive.
4. Confirm your priority puts you in the right order
If your data depends on another plugin’s data already being present, or your key
needs to land before home_cats is assembled, priority decides whether you see
the data you expect. The filter runs lowest priority first.
| If your subscriber… | Use priority |
|---|---|
| Only injects its own data, reads nothing | 10 (default) |
Must contribute before home_cats is built |
below 1000 |
| Reads data injected by another plugin | 100 |
| Validates already-assembled form state | 199 |
| Performs a full use-case override | 9999 |
Two failure modes here:
- You read a key that hasn’t been injected yet. If you run at priority 10 and
try to read another plugin’s listings (also at 10), you depend on registration
order — which is fragile. Move to priority 100 so you run after the default
cohort. - You registered at priority 1000. That slot is reserved by
mam-mainfor
home_cats. Registering there can collide with the home-screen assembly. Use
10 unless you have a specific reason to run later.
Use the $wp_filter dump from step 2 to see exactly where your callback sits
relative to the one whose data you depend on.
5. Confirm the cursor cache isn’t serving a stale “no change”
This is the subtlest one. The cursor cache lets the app skip the whole payload
when nothing has changed since its last request. mam-main keeps a single,
site-wide cursor — the mam_json_cursor option, a uniqid() value. The app
sends back the cursor it last received; if it still matches the server’s, the
pipeline short-circuits and returns a tiny
{"json_status":"no_new_data","cursor":...} stub — your key and every other key
are absent, and the app keeps its entire cached copy.
So if the endpoint returns a no_new_data stub even though you just edited the
data, the cursor never advanced. Causes:
-
You changed data on a path the cursor doesn’t watch.
JSON_Cursor_Manager::setup_hooks()auto-bumps the cursor on post, post-meta,
term, user, and user-meta writes. A change that goes through one of those
(saving a post, updating user meta) advances the cursor for free. But a change
to a plain option — one the cursor never watches — leaves it untouched, so the
app is told “nothing changed.” Bump it yourself after the write:JSON_Cursor_Manager::reset_cursor();reset_cursor()is a hammer — it writes a freshuniqid()to the one global
option, so it invalidates every app’s cache, not just yours, and forces a
full rebuild on the next request. That’s exactly what you want for debugging,
but call it only on real changes, not on everyupdate_option.
To prove the cursor is the culprit, force a full rebuild and see if your data
reappears. Turning on the Disable Caching app setting
(tsl-setting-disable_caching, General settings) makes the pipeline reset the
cursor on every request, so each fetch is a fresh build. If the key is correct
with caching disabled but the app gets a stale no_new_data otherwise, your
invalidation logic is the bug.
6. For form data: confirm the form cache isn’t stale
If the missing data is a form (a field you added, a relabeled field, a
changed setting), the phone-data cursor may be fine while a separate cache — the
per-form transformed-definition cache — is stale. mam-main caches each form’s
built mobile shape in a form_cache post-meta value on the post the form is
attached to; get_data_for_app() returns that cached blob on a hit without
re-running the transformation chain (unless it is called with
$exclude_cache = true).
Typical stale-form symptoms:
| Symptom | Likely cause |
|---|---|
| You edited a form, the app still shows the old version | The cache wasn’t invalidated for that form id |
| You added a custom field type, the app shows the GF default | Invalidation ran before your new filter was registered |
| You toggled per-field special handling, behavior is unchanged | The toggle isn’t part of the cache key |
| Theme colors changed, forms still show old colors | Color settings aren’t part of the cache key |
Quick diagnosis — clear the one form’s cached blob and re-fetch:
delete_post_meta( $post_id, 'form_cache' );
If the data appears after clearing it, the real fix is to clear form_cache
(or fetch with $exclude_cache = true) at whatever write site changed the
setting. mam-main already does exactly this on form submission (see
submit-app-form.php); a definition or setting change needs the same
invalidation.
7. Only now, check the client
If the key is present, correctly shaped, not null, and survives a
cache-bypassed request, the remaining possibility is that the app doesn’t know
how to read it. Server-side injection is only half the contract — the mobile
client must be built to consume the key and render it on a screen. Confirm with
the mobile team that:
- The app version in your hands actually reads this key.
- The key name and shape match what the client expects.
- The screen that should render it is reachable in this build.
A brand-new key ships and does nothing until the app is updated to read it.
Quick reference
| What you see | Most likely link | Go to |
|---|---|---|
| Key absent from raw JSON | Filter / return | Step 2 |
| Key present, app ignores it | JSON shape / types | Step 3 |
| Key reads wrong dependent data | Priority / order | Step 4 |
Whole response is a no_new_data stub |
Cursor cache | Step 5 |
| Form change not reflected | Form cache | Step 6 |
| Appears only with Disable Caching on | Cursor invalidation | Step 5 |
| Everything correct server-side | Mobile client | Step 7 |
Anti-patterns to avoid
- Debugging the app before confirming the key is in the raw JSON. Start at the
endpoint, not the device. reset_cursor()on everyupdate_option“to be safe” — that defeats the cache
for every user and turns every launch into a full rebuild.- Editing a form’s definition without clearing its
form_cachepost-meta, then
assuming the app will pick up the change. - Returning anything but
$data_arrayfrom the filter. - Shipping numeric ids or boolean flags and assuming the app will coerce them.
Related
- Mobile JSON shape (
mam-main) — the frozen top-level keys and primitive conventions - Cursor cache mechanism (
mam-main) — the single site-wide cursor, thecursorparameter,reset_cursor(), and the Disable Caching setting - Priority conventions for phone-data subscribers (
mam-main) — the full priority bands and ordering contracts - Form cache and invalidation (
mam-main) — theform_cachepost-meta,$exclude_cache, and stale-cache symptoms - Recipe: Inject custom data into the mobile JSON (
mam-main) — the supported way to add a key in the first place - Hook:
mam_get_phone_data_before_send— the filter reference
