Debug ‘my data isn’t showing up in the app’ (developer checklist)

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 the local_app_get_phone_data
    AJAX action on admin-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-main active. 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 null or a single space → that is a null
    your own data produced, not a cache signal; output shaping rewrites a null
    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", not 123. 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 ship true / false where 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-main for
    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 fresh uniqid() 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 every update_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 every update_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_cache post-meta, then
    assuming the app will pick up the change.
  • Returning anything but $data_array from the filter.
  • Shipping numeric ids or boolean flags and assuming the app will coerce them.

  • Mobile JSON shape (mam-main) — the frozen top-level keys and primitive conventions
  • Cursor cache mechanism (mam-main) — the single site-wide cursor, the cursor parameter, 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) — the form_cache post-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
Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!