Back to Knowledge Hub
Identity

Browsing History Seeding: Session vs Global History

How seeded browsing history differs from scripted navigation, user data directory reuse and history.pushState, and how history.length differs from the global history database.

BotBrowser Team

Documentation

Prefer the maintained product doc?

This article has a matching page in the docs center. Use the docs for the canonical setup flow, current flags, and long-term reference.

A browser session that has just launched has a very short past. The tab holds one entry in its back and forward stack, the address bar has nothing to suggest, and the history page may be empty. Teams that run repeatable browser tests sometimes want to start from a different state: a session that already carries some navigation depth, so that the code under test meets the same conditions on every run.

That goal is easy to state and easy to get wrong, because two different records are both called history. Below, the two records are separated, four ways of preparing a session are compared (scripted navigation, a reused user data directory, history.pushState and launch-time seeding with BotBrowser), the modes of --bot-inject-random-history are listed, and a short procedure shows how to check the result without mistaking one record for the other.

Two panels: session history of one tab, read by history.length, and the global history database shown in chrome://history, with a reminder to check both.

Two histories behind one word

Browsers use the word history for at least two different records, and most confusion about seeded history comes from mixing them up.

The first record is session history. Each tab keeps an ordered list of the pages it has shown, and the back and forward buttons walk along that list. Page scripts reach it through the History interface. According to MDN, history.length is a read-only integer that holds the number of entries in the session history, including the page that is currently loaded, and a page opened in a new tab reports 1. Calling history.back(), history.forward() or history.go() moves along the list, while history.pushState() and history.replaceState() change it without loading a new document.

The second record is global history. This is the browser-wide list of visited addresses that feeds the chrome://history page and the suggestions in the address bar, which Chromium calls the omnibox. It is kept with the profile data, every tab contributes to it, and a page script cannot read it. A number such as history.length tells you nothing about what it contains.

The two records answer different questions. Session history answers how many steps this tab has behind it. Global history answers which addresses this profile has visited. A test that only reads history.length observes the first record, and a test that opens chrome://history observes the second. Neither observation proves anything about the other, so a technique that adds an entry to the back stack does not automatically add a row to the history page, and a technique that fills the history page does not automatically change the number a script reads.

This matters for testing because application code reads both kinds of state, directly or indirectly. A single-page application may show its own back button only when history.length is above 1. A sign-in flow may decide where to send the visitor depending on whether an earlier entry exists. A tester who works by hand sees different address bar suggestions once the profile has some past. If a test environment always starts from the shortest possible past, the test only ever exercises the first-visit branch of that code. Seeding gives you a way to run the other branch on purpose.

One more property of session history is easy to forget: it belongs to a single tab. A popup, a second tab or a freshly opened window starts its own list. When a test reads history.length in a different tab from the one that was prepared, it reads a different list, and the value 1 in a new tab is expected behavior, not a sign that seeding failed.

Four ways to prepare a session

There are four common ways to give a session some history before the real test begins. They differ in which record they touch, in the side effects they cause and in how repeatable they are.

Scripted navigation

The most direct method is to visit pages. The test script opens a list of addresses, waits for each one to load and then starts the real work. This is the only method in the list that produces real page state: the visited addresses are exactly the ones you chose, redirects are recorded the way the site served them, and cookies and storage are filled by the sites themselves. It is also the method with the highest cost.

  • Time. Every page has to load, so preparation takes from several seconds to minutes, depending on the pages and the connection.
  • Network traffic. Each visit sends real requests, uses the bandwidth of whatever route the browser takes and may run into rate limits.
  • Side effects. Pages run their own scripts, set cookies and send analytics events before the test begins, so the test no longer starts from a clean state.
  • Variation. Load times, redirects and dynamic content differ between runs, so the resulting history is not the same from one run to the next.

Use scripted navigation when the test depends on specific visited addresses or on real page state. Nothing else on this list can provide either.

Reusing a user data directory

Pointing every launch at the same --user-data-dir keeps whatever the earlier run stored, including its global history database. The price is that it keeps everything else as well: cookies, local storage, cached files and service workers. You cannot carry forward only the history and reset the rest.

The test also stops being strictly repeatable, because the starting state is whatever the last run left behind. Run N then depends on runs 1 to N-1. Copying the directory from a known snapshot before every run removes the drift, at the cost of file handling and disk space. This approach suits tests that really are about long-lived state. It is a heavy way to get a history depth.

Calling history.pushState

A page script can call history.pushState(state, '', url) to add an entry to the tab's session history without loading anything. MDN notes that the new URL must have the same origin as the current one, so a script on one site cannot fill the stack with addresses from other sites, and MDN describes the call only in terms of the session history stack.

The call is a page API meant for single-page applications. It is not a way to build a browsing record. In a test it is useful for one purpose: reproducing the way a single-page application grows its own back stack, so that you can check how your code reacts when history.length rises without any page load.

Seeding at launch

The --bot-inject-random-history flag of BotBrowser (PRO tier) takes a different route. Instead of visiting pages, it creates synthetic entries at startup, before the first page loads. Because the entries are generated at startup rather than loaded from sites, preparation needs no scripted page loads. Because the count is controlled by the flag, two launches with the same value start from the same depth.

The documentation also states the scope clearly: injection applies to each new session, and the injected entries do not persist beyond the session lifetime. What the flag cannot do is let you choose the addresses. The entries are generated for you.

Modes of the seeding flag

The BotBrowser history-seeding documentation lists two ways to turn the flag on and one way to turn it off.

  • --bot-inject-random-history or --bot-inject-random-history=true is random mode. It injects a random number of entries from 2 to 7, which the documentation says gives a history.length of 3 to 8.
  • --bot-inject-random-history=15 is exact mode. It injects exactly that many entries, so history.length equals the count plus one: 15 entries give 16 on the first page. The documentation says the value must be between 1 and 25.
  • --bot-inject-random-history=false disables injection.

The same setting can come from the profile configuration through the injectRandomHistory key, where true selects random mode and a number selects exact mode. When both are present, the command line flag overrides the profile.

# Random mode: 2 to 7 entries
chromium-browser --bot-profile="path/to/profile.enc" --bot-inject-random-history
# Exact mode: 15 entries, history.length is 16 on the first page
chromium-browser --bot-profile="path/to/profile.enc" --bot-inject-random-history=15

Two properties of exact mode matter for reproducibility. The arithmetic is simple enough to assert in a test: the expected value on the first page is the count plus one. And random mode gives a range, so an assertion has to accept 3 to 8 instead of a single number. If a test needs one value, use exact mode and write the count into the launch command.

Because the entries are generated and the scope is the session, treat every launch as a fresh seeding. Do not expect the entries of yesterday's run to be waiting in a reused data directory. Keep the flag value in the launch command of every run instead, so the starting depth is written down next to the test.

The flag combines with the other seeding options in the documentation, such as --bot-cookies and --bot-bookmarks. See Cookie Management and Bookmark Injection for those, and Profile Management for keeping sets of profiles organized. For several browser contexts in one browser, the documentation lists per-context history as an ENT Tier3 feature.

Choosing an approach for a test run

Pick the approach from the question the test asks. The table summarizes what each method touches and what it costs.

ApproachWhat it touchesNetwork and side effectsRepeatabilityChoice of URLs
Scripted navigationSession history and global historyReal requests, page scripts, cookiesVaries with load times and contentYes
Reused data directoryEverything saved earlierNo new requests, but old state returnsDepends on earlier runsOnly through earlier runs
history.pushStateSession history of one tabNoneHighSame origin only
Seeding flagEntries counted by history.length, per the documentationGenerated at startup, not loaded from sitesHigh in exact modeNo

Cost is only part of the decision. Reproducibility is the other part, and it is where the methods differ most. A scripted visit depends on the sites you visit, a reused directory depends on earlier runs, and a seeded count depends only on the launch command. When a test failure has to be explained a month later, a single value in a command line is much easier to reason about than a history built from live pages. A few situations show how the choice plays out.

  • You need a back stack of a known depth. Use exact mode. The expected history.length is the count plus one, and the same launch command gives the same start every time.
  • You need some depth but not a specific number. Use random mode, and make assertions accept the range of 3 to 8.
  • You need specific addresses or real page state. Use scripted navigation. A page that reads a cookie set by an earlier visit, or a site that records visit counts, can only be prepared by really visiting it.
  • You are testing how a single-page application grows its stack. Call history.pushState inside the test, because that is the behavior under test.
  • You need all state to carry across runs. Reuse a copied data directory, and accept the drift that comes with it.

The approaches also combine. A common arrangement is to seed a depth with the flag and then script only the two or three navigations the test truly depends on. Setup stays short and the page state is real where it matters. When combining them, remember that every scripted navigation adds to the count. Expect history.length to rise by one for each new entry on top of the seeded value, and measure a baseline in your own build before writing exact assertions, because redirects and page scripts can add entries you did not plan for.

Checking what actually happened

Verification should read both records, because each one says something different. A run record with the fields below turns a one-off observation into something a colleague can repeat.

  1. Read the session value. Open a page and read history.length in the developer tools console. With exact mode and a count of 15, the first page reports 16, according to the documentation. In random mode the value should fall between 3 and 8.
  2. Open chrome://history in the same session and look at the entries. The documentation describes the effect on history.length and on the session. It does not describe the history page, so whether and how entries appear there is something to observe in your own build and write down, not something to assume.
  3. Navigate once and read the value again. It should rise by one for the new entry. If it does not, check whether the page redirected or replaced its own entry.
  4. Launch again with the same flags and repeat the checks. Seeded entries apply to each new session, so exact mode should give the same values, and random mode should stay inside its range.
  5. Write the launch value next to the result. Record the flag, the build, the expected length, the observed length and what the history page showed.

Write assertions as relations instead of magic numbers. In exact mode the expected value on the first page is the seeded count plus one, and every later navigation in the test adds one more. Expressing the check that way keeps it correct when someone changes the seeded count, and a failure then points at a specific difference between expected and observed values. In random mode, assert only the documented range, and log the observed value so that a surprising run can still be understood afterwards.

Most failed checks come from a short list of mistakes. The first is reading history.length in a different tab from the one that was prepared, where a new tab correctly reports 1. The second is comparing a seeded value against a number measured after extra navigations. The third is expecting entries to survive a restart, which the documentation says they do not. The fourth is writing an exact assertion against random mode.

If history.length stays at 1, the documentation points to three causes. The flag may have been passed outside the arguments array of the launcher. The license may not be active, in which case the BotBrowser console output reports a license error. Or the count may have been written without the equals sign, or outside the allowed range of 1 to 25.

A matching history.length shows that the session carries entries. It does not show how any particular site will treat the session, and no such claim is made here. Seeded history is one input to a test session, next to the cookies, bookmarks, locale and timezone that you configure separately.

Where BotBrowser fits and where it stops

Seeded history is a small part of a test plan. It fixes one variable, the depth of the session at launch, so that the other variables can be studied against a stable background.

BotBrowser (PRO) supports seeding synthetic browsing history at launch through --bot-inject-random-history, either a random number of entries or an exact count, so you can start every test session with a repeatable history depth without scripted page loads. It does not let you pick the injected URLs, cannot guarantee that the entries match a locale or look like real usage to any given site, and does not replace scripted navigation when specific visited URLs or real page state are required.

Check the current documentation for the tier, the count range and the history.length wording before you rely on them in a release, and use the MDN page for history.length as the reference for the session side of the behavior.

Public sources

#History#Injection#Identity#Browsing#Privacy

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.