GTM sessionStorage Read and Write
sessionStorage in GTM via Sandboxed JavaScript — Read & Write With Just One CSP Hash
This GTM container recipe works on Google Blogger / Blogspot as well as AEM, Sitecore, WordPress, React.js, and any other platform running a standard GTM snippet. It is a small, self-contained pattern — one Custom HTML tag plus two Sandboxed JavaScript Custom Templates — built specifically to keep sessionStorage access out of Content Security Policy's way. The JSON can be downloaded from Google Drive below.
A number of published community GTM templates manipulate localStorage directly, since it's a natural fit for a "utility" template. sessionStorage is the same API shape — getItem, setItem, removeItem, clear, key, length — but it isn't something a Sandboxed JavaScript template can reach on its own. Templates run in a locked-down sandbox with a fixed set of permitted built-in functions; there's no require('sessionStorage'). To read or write it at all, a template has to reach into the page's real window object.
The usual way to reach window from GTM is a Custom HTML tag with an inline <script> block. That works, but on any site enforcing a strict Content Security Policy with SHA-256 source hashes, every inline script needs its own hash — and that hash has to be recomputed and redeployed every single time the script's text changes by so much as a byte. This recipe avoids that entirely for the read/write logic: only one small script, the listener, ever needs a CSP hash. Everything else — every read, every write, every future tweak to the read/write logic — runs as GTM Sandboxed JavaScript and needs no hash at all.
What's Inside the Container
The exported container (gtm-session-storage-read-write.json) contains one tag folder — Process sessionStorage — holding one Custom HTML tag, one Custom Template tag, three variables, one trigger, and two Custom Templates (a variable-type template and a tag-type template).
| Tag | Type | Fires On | Purpose |
|---|---|---|---|
sessionStorage Listener |
Custom HTML | Early page-load trigger, priority 99, Once Per Event | Defines window.gtmSessionStorage — a small set of window-scoped helper functions wrapping the native sessionStorage API. This is the only tag in the recipe with an inline <script> element. |
sessionStorage Write |
Custom Template (sessionStorage Write) | sessionStorage Write trigger (Initialization) |
Calls window.gtmSessionStorage.setItem via callInWindow — no inline script, so no CSP hash required for this tag. |
| Variable | Type | Configuration |
|---|---|---|
sessionStorage Read | Custom Template (sessionStorage Read) | Action: getItem, Key: {{sessionStorage Name}} |
sessionStorage Name | Constant | Value: sessionTest |
sessionStorage Value | Constant | Value: something-written-2-sessionStorage |
| Trigger | Type | Condition |
|---|---|---|
sessionStorage Write |
Initialization | {{sessionStorage Read}} matches RegEx ^(undefined|null|0|false|NaN|)$ — i.e. fire the write only when the key doesn't already hold a value, so an existing session value is never clobbered on every page. |
The sessionStorage Listener: How It Works
This is the one tag in the recipe that injects real inline JavaScript. It defines a single window-scoped object, guards against re-initialization if the tag fires more than once, and wraps every native call in a try/catch so a browser that blocks storage (private browsing, storage-partitioning edge cases) fails safely instead of throwing.
<script>
(function() {
if (window.gtmSessionStorage) return; // avoid re-init on multiple tag fires
window.gtmSessionStorage = {
setItem: function(key, value) {
try {
sessionStorage.setItem(key, value);
return true;
} catch (e) {
console.error('gtmSessionStorage.setItem failed:', e);
return false;
}
},
getItem: function(key) {
try {
return sessionStorage.getItem(key);
} catch (e) {
console.error('gtmSessionStorage.getItem failed:', e);
return null;
}
},
removeItem: function(key) {
try {
sessionStorage.removeItem(key);
return true;
} catch (e) {
console.error('gtmSessionStorage.removeItem failed:', e);
return false;
}
},
clear: function() {
try {
sessionStorage.clear();
return true;
} catch (e) {
console.error('gtmSessionStorage.clear failed:', e);
return false;
}
},
key: function(index) {
try {
return sessionStorage.key(index);
} catch (e) {
console.error('gtmSessionStorage.key failed:', e);
return null;
}
},
length: function() {
// exposed as a function, not a property — callInWindow calls functions
try {
return sessionStorage.length;
} catch (e) {
console.error('gtmSessionStorage.length failed:', e);
return 0;
}
}
};
})();
</script>
This is the only script that needs a CSP hash. If your site enforces script-src with SHA-256 sources instead of a nonce, this is the single inline block you compute a hash for and add to your CSP header. Fire this tag once, early (priority 99, an early page-load trigger), and every Read/Write template call after it talks to window.gtmSessionStorage through GTM's own sandbox — no further hashes, ever.
The sessionStorage Read Template (Variable)
sessionStorage Read is a MACRO (variable) type template. It exposes three read-only operations against the helper object, gated behind queryPermission checks so the template fails closed — returning undefined and logging to the console — if the container's permission grant for that specific function is missing.
| Action | Parameter | Calls | Returns |
|---|---|---|---|
| Get Item | Key (text) | window.gtmSessionStorage.getItem(key) | Stored string, or null |
| Get Key By Index | Index (non-negative number) | window.gtmSessionStorage.key(index) | Key name at that index, or null |
| Get Length | — | window.gtmSessionStorage.length() | Number of stored items |
const callInWindow = require('callInWindow');
const queryPermission = require('queryPermission');
const makeString = require('makeString');
const makeNumber = require('makeNumber');
const logToConsole = require('logToConsole');
const action = data.action;
if (action === 'getItem') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.getItem')) {
return callInWindow('gtmSessionStorage.getItem', makeString(data.key));
}
logToConsole('Session Storage: Read - missing execute permission for gtmSessionStorage.getItem');
return undefined;
}
if (action === 'key') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.key')) {
return callInWindow('gtmSessionStorage.key', makeNumber(data.index));
}
logToConsole('Session Storage: Read - missing execute permission for gtmSessionStorage.key');
return undefined;
}
if (action === 'length') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.length')) {
return callInWindow('gtmSessionStorage.length');
}
logToConsole('Session Storage: Read - missing execute permission for gtmSessionStorage.length');
return undefined;
}
logToConsole('Session Storage: Read - unknown action: ' + action);
return undefined;
The sessionStorage Write Template (Tag)
sessionStorage Write is a TAG type template, so it calls data.gtmOnSuccess() / data.gtmOnFailure() instead of returning a value. It covers the three mutating operations.
| Action | Parameters | Calls |
|---|---|---|
| Set Item | Key (text, required), Value (text) | window.gtmSessionStorage.setItem(key, value) |
| Remove Item | Key (text, required) | window.gtmSessionStorage.removeItem(key) |
| Clear All | — | window.gtmSessionStorage.clear() |
const callInWindow = require('callInWindow');
const queryPermission = require('queryPermission');
const logToConsole = require('logToConsole');
const makeString = require('makeString');
const action = data.action;
if (action === 'setItem') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.setItem')) {
callInWindow('gtmSessionStorage.setItem', makeString(data.key), makeString(data.value));
data.gtmOnSuccess();
} else {
logToConsole('Session Storage: Write - missing execute permission for gtmSessionStorage.setItem');
data.gtmOnFailure();
}
} else if (action === 'removeItem') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.removeItem')) {
callInWindow('gtmSessionStorage.removeItem', makeString(data.key));
data.gtmOnSuccess();
} else {
logToConsole('Session Storage: Write - missing execute permission for gtmSessionStorage.removeItem');
data.gtmOnFailure();
}
} else if (action === 'clear') {
if (queryPermission('access_globals', 'execute', 'gtmSessionStorage.clear')) {
callInWindow('gtmSessionStorage.clear');
data.gtmOnSuccess();
} else {
logToConsole('Session Storage: Write - missing execute permission for gtmSessionStorage.clear');
data.gtmOnFailure();
}
} else {
logToConsole('Session Storage: Write - unknown action: ' + action);
data.gtmOnFailure();
}
Why access_globals Instead of a Storage-Specific Permission
Both templates request the access_globals permission, scoped to exactly six named window paths: gtmSessionStorage.getItem, .setItem, .removeItem, .clear, .key, and .length — each with read, write, and execute granted individually. This is the standard pattern for reaching a window-scoped helper from a Sandboxed JavaScript template: the template never touches sessionStorage directly, it calls a named function on window that the Listener tag already defined, and GTM's permission system whitelists that exact function name at template-approval time. Anyone reviewing the container in GTM's UI can see precisely which six window paths the templates are allowed to touch — nothing broader.
Demo Wiring: sessionTest
The container ships with a working example wired end-to-end so you can see the pattern fire in Preview mode immediately after import:
- The
sessionStorage Readvariable checks for a key namedsessionTest(from thesessionStorage Nameconstant). - The
sessionStorage Writetrigger fires only when that variable currently resolves to an empty/falsy value — meaning the key hasn't been set yet in this session. - When the trigger fires, the
sessionStorage Writetag setssessionTesttosomething-written-2-sessionStorage(thesessionStorage Valueconstant). - On any later page in the same tab/session,
sessionStorage Readnow resolves to that string, the trigger's condition is no longer met, and the write tag does not fire again — the value persists untouched until the browser tab's session ends.
Swap the two constant variables for real values, and add more actions/keys to the two templates as needed — the read/write logic scales to any number of sessionStorage keys without adding a single additional line of inline script or a single additional CSP hash.
How to Import
- Download
gtm-session-storage-read-write.jsonfrom the Google Drive link above. - In GTM, go to Admin → Import Container.
- Choose the downloaded JSON file, select the workspace to import into, and pick Merge (not Overwrite) if you already have other tags in the container.
- When prompted for conflicting Custom Templates, choose Rename if you want to keep both, or overwrite if this is a clean container.
- Confirm the import. You should now see the Process sessionStorage folder containing the
sessionStorage Listenertag, thesessionStorage Writetag, thesessionStorage Writetrigger, and the three variables. - Check Templates in the left nav — you should see two new Custom Templates: sessionStorage Read (Variable) and sessionStorage Write (Tag).
- If your site enforces CSP with SHA-256 sources, compute the hash for the
sessionStorage Listenertag's script block and add it to yourscript-srcdirective. No other tag in this recipe requires a CSP change. - Open GTM Preview mode and load a page. Confirm the
sessionStorage Listenertag fires first, then thesessionStorage Writetag fires on the first page of a new session. - In the browser DevTools console, run
sessionStorage.getItem('sessionTest')and confirm it returnssomething-written-2-sessionStorage. - Reload the page — the write trigger should not fire again, since
sessionStorage Readnow resolves to a truthy value. - Publish the container version once everything checks out in Preview.
The full source — container JSON and documentation — is also published on GitHub for anyone who wants to fork the read/write action list, add new sessionStorage keys, or adapt the same callInWindow pattern to a different window-scoped helper entirely.
Comments
Post a Comment