Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save 0xdevalias/f3dcb0ef45ed03472c02dab6975a7971 to your computer and use it in GitHub Desktop.

Select an option

Save 0xdevalias/f3dcb0ef45ed03472c02dab6975a7971 to your computer and use it in GitHub Desktop.
Some notes and observations from debugging theatre ticket website / web app frontends; explicitly for learning purposes and better understanding web app development / debugging.

Debugging Theatre Ticket Website / Web App Frontends

Some notes and observations from debugging theatre ticket website / web app frontends; explicitly for learning purposes and better understanding web app development / debugging.

Table of Contents

Disclaimer

These notes and observations are purely for learning purposes, and understanding web app development / debugging.

Tools

Browser

Browser Extensions

Shell

  • https://github.com/jqlang/jq
    • jq

    • Command-line JSON processor

    • jq is a lightweight and flexible command-line JSON processor akin to sed,awk,grep, and friends for JSON data. It's written in portable C and has zero runtime dependencies, allowing you to easily slice, filter, map, and transform structured data.

  • https://github.com/tomnomnom/gron
    • gron

    • Make JSON greppable!

    • gron transforms JSON into discrete assignments to make it easier to grep for what you want and see the absolute 'path' to it. It eases the exploration of APIs that return large blobs of JSON but have terrible documentation.

    • Quck usage examples:
      • gron "URL" | fgrep "foo.bar"
      • pbpaste | gron | subl -n

Useful Snippets

JavaScript

Extract Next.JS's __NEXT_DATA__ from initial page HTML:

// Raw stringified JSON
nextDataJson = $('#__NEXT_DATA__').innerText

// Copy it to the clipboard
copy(nextDataJson)

// Parse it into a JS Object if that is useful
nextDataObject = JSON.parse(nextDataJson)

Search for a known key or value in a JavaScript object, and get it's path:

First define these helper functions:

function* walkObject(value, path = []) {
  if (value === null || typeof value !== "object") return;

  for (const [rawKey, child] of Object.entries(value)) {
    const key = Array.isArray(value) ? Number(rawKey) : rawKey;
    const childPath = [...path, key];

    yield { path: childPath, key, value: child };
    yield* walkObject(child, childPath);
  }
}

const findInObject = (object, ...filters) =>
  [...walkObject(object)].flatMap(entry => {
    const matches = [];

    for (const [label, test] of filters) {
      if (test(entry)) matches.push(label);
    }

    return matches.length
      ? [{
          path: entry.path,
          value: entry.value,
          ...(filters.length > 1 && {
            matched: matches.join(" and "),
          }),
        }]
      : [];
  });

const jsonEquals = (a, b) =>
  a === b ||
  (
    a !== null &&
    b !== null &&
    typeof a === "object" &&
    typeof b === "object" &&
    Array.isArray(a) === Array.isArray(b) &&
    Object.keys(a).length === Object.keys(b).length &&
    Object.keys(a).every(
      key => Object.hasOwn(b, key) && jsonEquals(a[key], b[key])
    )
  );

And then you can use them like:

// Assume the parsed JSON is stored in `nextDataObject`:
// const nextDataObject = JSON.parse(nextDataJson);

// Search by key:
// Replace "targetKey" with the exact key name to find.
findInObject(
  nextDataObject,
  ["key", ({ key }) => key === "targetKey"],
);

// Search by string value:
// Replace "targetValue" with the exact string value to find.
findInObject(
  nextDataObject,
  ["value", ({ value }) => value === "targetValue"],
);

// Search by non-string JSON value:
// Replace 123 with a number, boolean, null, array, or object.
// Examples: 123, true, null, [1, 2, 3], { enabled: true }
findInObject(
  nextDataObject,
  ["value", ({ value }) => jsonEquals(value, 123)],
);

// Search for an exact string match in either a key name or a value:
// Replace "target" with the key or string value to find.
findInObject(
  nextDataObject,
  ["key", ({ key }) => key === "target"],
  ["value", ({ value }) => value === "target"],
);

// Search for a partial string match in either a key name or string value:
// Replace "target" with the substring to find.
// Matching is case-sensitive.
findInObject(
  nextDataObject,
  [
    "key",
    ({ key }) =>
      typeof key === "string" &&
      key.includes("target"),
  ],
  [
    "value",
    ({ value }) =>
      typeof value === "string" &&
      value.includes("target"),
  ],
);

// Search for a partial string match in either a key name or string value:
// Replace "target" with the substring to find.
// Matching is case-insensitive.
{
  const search = "target".toLowerCase();

  findInObject(
    nextDataObject,
    [
      "key",
      ({ key }) =>
        typeof key === "string" &&
        key.toLowerCase().includes(search),
    ],
    [
      "value",
      ({ value }) =>
        typeof value === "string" &&
        value.toLowerCase().includes(search),
    ],
  );
}

Shell

Search for a known key or value in a JSON blob via gron, and get it's path:

# Search for a partial string match in either a key name or string value:
# Replace "target" with the substring to find.
# A matching key may appear anywhere within the nested path.
# Matching is case-sensitive.
pbpaste | gron | grep -F 'target'
# Alternatives:
# pbpaste | gron | rg -F 'target'
# pbpaste | gron | fgrep 'target'  # Legacy alias for grep -F

# Search for a partial string match in either a key name or string value:
# Replace "target" with the substring to find.
# A matching key may appear anywhere within the nested path.
# Matching is case-insensitive.
pbpaste | gron | grep -Fi 'target'
# Alternatives:
# pbpaste | gron | rg -Fi 'target'
# pbpaste | gron | fgrep -i 'target'  # Legacy alias for grep -F

# Search by key:
# Replace "targetKey" with the exact key name to find anywhere in the path.
# If the key contains nested content, all descendant lines beneath it will match.
pbpaste | gron | grep -E '(\.targetKey|\["targetKey"\])(\.|\[| =)'
# Alternatives:
# pbpaste | gron | rg '(\.targetKey|\["targetKey"\])(\.|\[| =)'

# Search by string value:
# Replace "targetValue" with the exact string value to find.
pbpaste | gron | grep -F '= "targetValue";'
# Alternatives:
# pbpaste | gron | rg -F '= "targetValue";'
# pbpaste | gron | fgrep '= "targetValue";'  # Legacy alias for grep -F

# Search by non-string JSON value:
# Replace '123' with the exact number, boolean, or null value to find.
# Examples: '123', 'true', 'false', 'null'
pbpaste | gron | grep -E '= 123;$'
# Alternatives:
# pbpaste | gron | rg '= 123;$'

# Search for an exact string match in either a key name or a value:
# Replace "target" with the exact key or string value to find.
# A matching key may appear anywhere within the nested path.
pbpaste | gron | grep -E '(\.target|\["target"\])(\.|\[| =)|= "target";$'
# Alternatives:
# pbpaste | gron | rg '(\.target|\["target"\])(\.|\[| =)|= "target";$'

Search for a known key or value in a JSON blob via jq, and get it's path:

# Search by key:
# Replace "targetKey" with the exact key name to find.
pbpaste | jq -c --arg key "targetKey" '
  paths as $path
  | select($path[-1] == $key)
  | {
      path: $path,
      value: getpath($path)
    }
'

# Search by string value:
# Replace "targetValue" with the exact string value to find.
pbpaste | jq -c --arg value "targetValue" '
  paths(scalars) as $path
  | select(getpath($path) == $value)
  | {
      path: $path,
      value: getpath($path)
    }
'

# Search by non-string JSON value:
# Replace '123' with a number, boolean, null, array, or object.
# Examples: '123', 'true', 'null', '[1,2,3]', '{"enabled":true}'
pbpaste | jq -c --argjson value '123' '
  paths as $path
  | select(getpath($path) == $value)
  | {
      path: $path,
      value: getpath($path)
    }
'

# Search for an exact string match in either a key name or a value:
# Replace "target" with the key or string value to find.
pbpaste | jq -c --arg search "target" '
  paths as $path
  | getpath($path) as $value
  | select(
      ($path[-1] == $search)
      or
      ($value == $search)
    )
  | {
      path: $path,
      value: $value,
      matched: (
        if ($path[-1] == $search) and ($value == $search) then "key and value"
        elif $path[-1] == $search then "key"
        else "value"
        end
      )
    }
'

# Search for a partial string match in either a key name or string value:
# Replace "target" with the substring to find.
# Matching is case-sensitive.
pbpaste | jq -c --arg search "target" '
  paths as $path
  | getpath($path) as $value
  | select(
      (($path[-1] | type) == "string" and ($path[-1] | contains($search)))
      or
      (($value | type) == "string" and ($value | contains($search)))
    )
  | {
      path: $path,
      value: $value,
      matched: (
        if (($path[-1] | type) == "string" and ($path[-1] | contains($search)))
           and (($value | type) == "string" and ($value | contains($search)))
        then "key and value"
        elif (($path[-1] | type) == "string" and ($path[-1] | contains($search)))
        then "key"
        else "value"
        end
      )
    }
'

# Search for a partial string match in either a key name or string value:
# Replace "target" with the substring to find.
# Matching is case-insensitive.
pbpaste | jq -c --arg search "target" '
  ($search | ascii_downcase) as $needle
  | paths as $path
  | getpath($path) as $value
  | (($path[-1] | type) == "string"
      and ($path[-1] | ascii_downcase | contains($needle))) as $key_match
  | (($value | type) == "string"
      and ($value | ascii_downcase | contains($needle))) as $value_match
  | select($key_match or $value_match)
  | {
      path: $path,
      value: $value,
      matched: (
        if $key_match and $value_match then "key and value"
        elif $key_match then "key"
        else "value"
        end
      )
    }
'

Sites

Master of Tickets

It seems to be a Next.JS web app:

  • $('#__NEXT_DATA__').innerText

There also seem to be some interesting globals such as:

  • window.__INITIAL_STATE__
  • window.__STATE__
    • window.__STATE__.api
    • window.__STATE__.basket
    • window.__STATE__.eventInfo
      • window.__STATE__.eventInfo.discoveryUrl
      • window.__STATE__.eventInfo.name
      • window.__STATE__.eventInfo.dates
      • window.__STATE__.eventInfo.venue
      • window.__STATE__.eventInfo.secnames
      • window.__STATE__.eventInfo.restrictSingleSeats
      • etc
    • window.__STATE__.offers
    • window.__STATE__.quickpicks
    • window.__STATE__.ticketSelection
      • window.__STATE__.ticketSelection.sections
      • window.__STATE__.ticketSelection.priceToSections
      • window.__STATE__.ticketSelection.ticketTypes
      • etc
    • window.__STATE__.unlockToken
    • window.__STATE__.smartQueue
    • etc
  • window.__REDUX_STORE__
    • window.__REDUX_STORE__.getState()
  • etc

Some potentially interesting JS source files for further reading:

  • https://{SUBDOMAIN}.{STATIC-CONTENT-DOMAIN}.{TLD}/{BUILD-ID}/_next/static/chunks/seatmap.{HASH}.js
    • s.jsx)(e0.$t, {
        onClick: () => p(e.ticketTypeId, e.offerId),
        __: i
      })
  • https://{SUBDOMAIN}.{STATIC-CONTENT-DOMAIN}.{TLD}/{BUILD-ID}/_next/static/chunks/pages/event/%5BeventId%5D-{HASH}.js
    • let s = l()
    • {config: u, eventInfo: m, brand: B} = s
    • if (m.restrictSingleSeats &&
  • https://checkout.{DOMAIN}.{TLD}/js/sdk.js
    • acceptSplit: Boolean("true" === e.acceptNonAdjacent || c),
    • const E = x.tickets
    • Among likely many other things, it seems to include some GraphQL API things such as:
      • mutation: $totalReservationAmount:

See Also

My Other Related Deepdive Gist's and Projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment