FullStory Data Layer Observer (DLO) is a small but powerful JavaScript utility that makes integrating analytics data easier. Through a flexible syntax and rules-driven approach, you can reference data from a data layer, perform intermediate changes, and send the result to FullStory - all without writing any custom JavaScript.
Add Data Layer Observer to your web site or web app by including the following snippet.
<script>
window['_dlo_appender'] = 'fullstory';
window['_dlo_beforeDestination'] = [{ name: 'convert', enumerate: true, preserveArray: true, index: -1 },{ name: 'suffix' }];
window['_dlo_previewMode'] = true; // set to false in production to send data to the destination
window['_dlo_readOnLoad'] = true; // see docs on usage
window['_dlo_validateRules'] = true;
window['_dlo_rules'] = [
// add your rules here
];
</script>
<script async="true" src="https://edge.fullstory.com/datalayer/v2/latest.js"></script>
The examples folder has a variety of samples of Data Layer Observer in action. To run examples locally, install node.js and perform the following after cloning or downloading this repo.
cd fullstory-data-layer-observer/
npm install # do this only the first time
npm run examples
Then open your browser to http://127.0.0.1:8080/examples
. You can experiment with adjusting the rules or the data in the index.html
files and watch the output in your browser.
Before using DLO, it’s important to understand what’s in your data layer. This can be as simple as visiting a web page in a browser like Chrome and opening the JavaScript Console (option
+ cmd
+ j
). Try typing digitalData
or dataLayer
or s
in the text field next to the caret and hit return
. If something is printed that does not look like an error, that’s the data layer. You can click the small triangle twistie to further explore what’s in the data layer.
More sophisticated websites will often have a dictionary of what’s in a data layer and the meaning of the data. Always consult your analytics team before using data found in a data layer.
Sensitive, private, and confidential information should never be added to a data layer. Various 3rd party libraries reside on most websites and have access to such information. DLO uses the following approaches to additionally prevent unwanted data from being transmitted.
- A rule-based design requires an admin to be intentional about data recording. An admin must create and load a rule to collect any information from a data layer. Recording is not automatic.
- Despite being intentional, it’s possible that domain teams may later add unwanted properties to a previously approved object in the data layer. DLO’s selector syntax allows an admin to specify which properties within an object are included or omitted as well as filtering techniques to further prevent recording unwanted data.
DLO is a JavaScript asset that is included on a web page. FullStory hosts versions of DLO on our CDN. Versioned releases have the naming convention <version>.js
, and the most recent version is named latest.js
:
If you would like the most up to date version of DLO on your site always, use latest.js
. If you'd rather use stable releases and perform manual upgrades, use <version>.js
.
To include DLO on a webpage, simply add the script to source code or load the script dynamically using a tag manager. The DLO asset should be loaded after the FullStory recording snippet has been added to the page.
If you would prefer to self-host the asset, use the npm run build
command to create dlo.js
and dlo.min.js
files. The latter is a minified script suitable for production. You should also gzip compress the asset and set appropriate Cache-Control
headers when self-hosting.
FullStory’s hosted asset is around 8.4KB minified, compressed and has Cache-Control
set to one month for versioned scripts and ten minutes for the latest script.
DLO is configurable by adding relevant options as window
properties to the page prior to loading the script. The following sample demonstrates a configuration suitable for testing a CEDDL rule.
<script>
window['_dlo_beforeDestination'] = [{ name: 'convert', enumerate: true, preserveArray: true, index: -1 },{ name: 'suffix' }];
window['_dlo_previewMode'] = true;
window['_dlo_readOnLoad'] = true;
window['_dlo_validateRules'] = true;
window['_dlo_rules'] = [{
id: 'fs-event-ceddl-cart',
description: 'send CEDDL cart cartID and price properties to FS.event as a "View Cart" event',
source: 'digitalData.cart[(cartID,price)]',
operators: [{ name: 'insert', value: 'View Cart' } ],
destination: 'FS.event'
}];
</script>
Additional configuration can be added using the below options.
Option | Type | Default | Description |
---|---|---|---|
_dlo_appender | LogAppender or string | console |
Defines a custom log appender to redirect log messages. |
_dlo_beforeDestination | OperatorOptions | undefined |
An optional operator that is always used just before before the destination. |
_dlo_logLevel | number | 1 |
Log messages at this level and below will be logged by the LogAppender. Defaults to WARN. |
_dlo_previewDestination | string | 'console.log' |
Output destination using rule selector syntax for use with previewMode. |
_dlo_previewMode | boolean | true |
Redirects output from a destination to previewDestination when testing rules. |
_dlo_readOnLoad | boolean | false |
When true reads data layer target(s) and emits the initial value(s). |
_dlo_rules | array | [] |
Anything that starts with _dlo_rules is read as a rules array. |
_dlo_validateRules | boolean | true |
When true validates rules to prevent processing invalid options. |
_dlo_urlValidator | function | (url) => boolean |
Function used to validate the page URL before executing the rules. The default tests window.location.href . |
_dlo_telemetryProvider | TelemetryProvider | DefaultTelemetryProvider |
Measures performance timings and client errors. |
_dlo_telemetryExporter | TelemetryExporter | nullTelemetryExporter |
Exports performance timings measured by the DefaultTelemetryProvider to a custom destination. Ignored when _dlo_telemetryProvider is defined. |
To observe a data layer, DLO uses rules to define what to observe and how to handle data. A variety of sample rules that are compatible with existing data layers can be found in the examples/rules folder.
A rule is composed of three primary pieces of information:
source
targets an object in the data layer using a selector. The selector can also be used to choose which data in an object is recorded.destination
declares a function, which acts as a destination for data. The function is an API that already exists on the page - likeFS.event
.operators
provide intermediate transformations of the data between the source and destination.
A rule is expressed as JSON and multiple rules are included in a _dlo_rules
list.
window['_dlo_rules'] = [
{
id: 'fs-uservars-ceddl-user-all',
description: 'send all CEDDL user properties to FS.setUserVars',
source: 'digitalData.user.profile[0]',
operators: [ { name: 'flatten' } ],
destination: 'FS.setUserVars'
},
{
id: 'fs-event-ceddl-transaction-id-total',
description: 'send CEDDL transaction transactionID and total properties to FS.event as an "Order Completed" event',
source: 'digitalData.transaction[(transactionID,total)]',
operators: [ { name: 'flatten' }, { name: 'insert', value: 'Order Completed' } ],
destination: 'FS.event'
}
];
The above sample contains two rules that respectively send all properties in the digitalData.user.profile
object to the function FS.setUserVars
and sends specific properties from the digitalData.transaction
object to the FS.event
function. The mechanics of how this is done is explained in the Operator Tutorial in greater detail.
Tip: While not required, a best practice is to include an
id
in every rule. Theid
should uniquely identify the rule for troubleshooting purposes. Adescription
is also optional but often helps explain the intent of a rule.
Each rule provides a set of options for configuration. Options with an asterisk are required.
Option | Default | Description |
---|---|---|
source * |
undefined |
Data layer source object using selector syntax. |
destination * |
undefined |
Destination function using selector syntax. |
debounce |
250 |
Milliseconds that must pass before multiple, sequential changes to a data layer are handled (increase for highly active data layers) |
debug |
false |
Set to true if the rule should print debug for each operator transformation. |
description |
undefined |
Text description of the rule. |
id |
undefined |
Unique identifier for the rule. |
maxRetry |
5 |
The maximum number of attempts to search for an undefined data layer or test the waitUntil predicate. |
monitor |
true |
Set to true to monitor property changes or function calls |
operators |
[] |
List of operators that transform data before a destination. |
readOnLoad |
false |
Rule-specific override for window[‘_dlo_readOnLoad’] . |
url |
undefined |
Specifies a regular expression that enables the rule when the page URL matches. |
waitUntil |
undefined |
Waits a desired number of milliseconds or predicate function's truthy return type before registering the rule. |
Tip: Use
url
to limit when data is read from a data layer and enhance performance.
The configuration option window['_dlo_readOnLoad'] = true;
can be used on static data layers, which should exist on the page prior to loading DLO. Many data layers are however dynamic: values change as the user interacts with the page. By default, DLO will attempt to monitor for changes to a data layer's object or any property within an object. (See the details on source selection on how this is done.) An observed change will cause the data to be handled. Note that property changes are debounced, which allows multiple property changes to a single object appear as a single data layer event.
Functions can be observed as well (e.g. trackEvent()
). When functions are called, the arguments passed to the function will be provided to data handlers.
As mentioned, source
is an important aspect of a rule: it defines which data layer subject to observe or read an initial value from. A source
can be a JavaScript object with name value pairs (digitalData.user
) or a function (s.event
).
The source
property uses a custom selector syntax. It’s most often seen as dot notation such as digitalData.cart
, which can be read as, "Find the cart object inside the digitalData object that exists on the page." Selector syntax is however much more expressive to allow a variety of common use cases.
Selector | Example | Description |
---|---|---|
parent.child.grandchild | digitalData.user |
Returns the last object from dot notation. |
object[index] | digitalData.product[-1] |
Returns the object at index (a negative index steps back from the end, so -1 is the last item in an array). |
object[(property, ...)] | digitalData.cart[(cartID,price)] |
Returns an object with only the listed properties. |
object[!(property, ...)] | digitalData.page.pageInfo[!(destinationURL,referringURL)] |
Returns an object with the listed properties removed. |
object[^(property, ...)] | digitalData.cart.price[^(shipping)] |
Returns an object with properties whose names begin with property . |
object[$(property, ...)] | digitalData.page.pageInfo[$(Date)] |
Returns an object with properties whose names end with property . |
object[?(property, ...)] | digitalData.cart[?(cartID)] |
Returns the object or null if the object does not have property. |
object[?(property=value, ...)] | digitalData.cart.price[?(basePrice>=10)] |
Returns the object or null if the object's property does not compare to value . Comparison can be = , <= , >= , < , > , != , !$ , =$ , !^ , and =^ . Use the value undefined to query if a property = or != undefined. |
Selector syntax can be combined to create sophisticated queries to the data layer. For example, digitalData.products[-1].attributes.availability[?(pickup)]
can be read as, "From the products list, return the last product's availability if it has the pickup
property." This usage of selection can be helpful to record only significant events and disregard others.
The properties in the object returned from source selection will be monitored for changes. For example, using the selector digitalData.cart
will observe all properties in the digitalData.cart
object because no refinement of the data is done by the selector. Alternatively, digitalData.cart[(cartID,price)]
will only observe only cartID
and price
in digitalData.cart
. Similarly, digitalData.cart.price[^(shipping)]
would observe all properties beginning with shipping. Selectors allow you to be specific about which data to send to a destination and only monitor changes on desired properties.
Tip: A selector returns the most current subject from the data layer at the lowest level. For example,
digitalData.cart.price[?(basePrice>=10)]
returns theprice
object - notcart
. If you needcart
returned, usedigitalData.cart
as the selector and the use the query operator.
For every source
there must also be a destination
. A destination
is a JavaScript function that is located using selector syntax. (Though it’s often less expressive since dot notation is usually enough to find the function.) A simple example of a destination is console.log
, which would print the output of a data layer to the console.
Because destination
is a function, this gives DLO a very flexible way to hand off data to a third party. You can think of this hand off as DLO calling a specific JavaScript function with arguments. The arguments are the data emitted from the data layer and anything added by operators
. In the simple console.log
destination example, the object taken from the data layer would call console.log(object)
, which would simply print the object on the console.
A more practical example is calling FullStory’s event function by setting the destination to FS.event
. There’s one catch though: the object emitted from the data layer must be the second argument for FS.event
. To add the first argument and move the object to the second, an operator will be used. The next section talks about operators and the Operator Tutorial explains the process in more detail.
If a source emits data and a destination is the recipient, what makes the two compatible? The answer is an operator
. An operator is designed to perform light transformation of an object emitted from the data layer so that it can be received by a destination. Operators are chained together using the operators
property in a rule. Each operator performs a small transformation and provides the result to the next operator. An operator can also choose to not pass along data (by returning null
) at which point data handling stops and the destination is not called.
While it may take a moment to familiarize yourself with operators, they are designed to do the work that would otherwise have been custom JavaScript. Instead of writing code, you’ll configure operators to do the work.
See the Operator Tutorial for an in-depth guide on using a few key operators.
Click an operator name for additional documentation.
Name | Description |
---|---|
convert | Formats a value to a bool, int, real, or string. |
fan-out | Executes subsequent operators on each item in a list. |
flatten | Recursively flattens all properties into an object. |
function | Executes a function and returns the result. |
insert | Inserts a value into a list at a specified position. |
parse | Parses a string value contained in a specified property into individual parts. |
query | Executes queries using selector syntax to return specific data within an object. |
rename | Renames existing properties to desired properties. |
suffix | Infers and appends a type suffix to an object’s properties. |
Every operator requires the name
property. Additional options can be found by viewing an operator's documentation.
Tip: If an operator is required every time, use the
window['_dlo_beforeDestination']
configuration option. This will define an operator that is always run just prior to a destination. This can make rule writing less tedious and is applicable to scenarios likesuffix
where aFS
destination always requires a suffixed payload.
Rule writing can sometimes take a few attempts to get it right. Fortunately, the following two options can help.
- When the configuration option
_dlo_previewMode
is set totrue
, output will be written toconsole.log
rather than thedestination
. - When a particular rule's
debug
property is set totrue
, the incremental transformations performed byoperators
and additional logging will be written toconsole.debug
.
Viewing the JavaScript console with the above option set to true
prints the following example.
dataLayer.transaction[!(items)] handleData entry
[{"transactionID":"6691791616826663170","total":{"grandTotal":"45.59", ... }}]
[0] flatten output (numKeys=7 sizeOfValues=94 sizeOfPayload=342)
[{"transactionID":"6691791616826663170","grandTotal":"45.59", ... }]
[1] convert output (numKeys=10 sizeOfValues=114 sizeOfPayload=434)
[{"transactionID":"6691791616826663170","grandTotal":45.59, ... }]
[2] insert output
["Order Completed",{"transactionID":"6691791616826663170","grandTotal":45.59, ... }]
[3] suffix output
["Order Completed",{"transactionID_str":"6691791616826663170","grandTotal_real":45.59, ... }]
[4] function output
[null]
dataLayer.transaction[!(items)] handleData exit
[null]
Order Completed {transactionID_str: "6691791616826663170", grandTotal_real: 45.59, ... }
The data emitted from a data layer can be seen below the handleData entry
line. Each operator will print its respective output. The order of operators is denoted by [n]
just before the operator's name. Eventually [null]
is printed because this is the result of the function call. The Order Completed line with the corresponding object is the console log showing the arguments that were sent to the function call.
In addition to showing the data conversions, debug includes the following statistics.
- The number of properties in the object shown as
numKeys
. - The size (assuming UTF16) of the values in the object shown as
sizeOfValues
. - The size (assuming UTF16) of the object when it is stringified shown as
sizeOfPayload
.
Once rules have been verified in a test environment, they can be moved to production. New rules can be added to any existing rules such as those in window['_dlo_rules']
. Alternatively, new rules can be placed in their own rule set to allow contribution from different teams or parts of a site. This can be done by creating a new configuration option that begins with _dlo_rules
- for example window['_dlo_rules_cart_and_checkout']
. When DLO initializes all properties that begin with _dlo_rules
will be processed.
After new rules are deployed, visit the site and ensure the data is being sent to the destination. One approach is to simply review in FullStory that the custom event or user variable has been set. If data is not found, set the debug
property to true
for any problematic rules. If problems still persist, see the Monitoring section to verify something else isn’t at fault.
The Quick Start snippet provides a FullStory log appender. When a log event is generated by DLO, it will be sent to FullStory as a custom event. You should periodically review the Data Layer Observer
custom event to monitor data layer events. Logging can be updated by setting window['_dlo_logLevel']
to the appropriate level value. Messages for the chosen log level and below will be logged.
0
- Error1
- Warn2
- Info3
- Debug
Logging can be disabled completely by setting the log level to -1
.
The following properties are included in a log event.
level
corresponds to the log level.message
provides information about the log event.context
may provide the following addition detail for troubleshooting purposes.rule
contains the rule ID.source
selector used in a rule.operator
name that generated the log event.path
to the object in the data layer.property
that relates to the log event.selector
selector used in a target.reason
underlying error message - usually obtained from a JavaScript Error.
Be mindful of rate limiting when sending logs to FullStory; however, DLO defaults to log only errors and warnings to reduce the likelihood of rate limiting.
A custom log appender can also be registered. For details on writing a custom appender, see the LogAppender
interface and ConsoleAppender
class in logger.ts.
By default, performance timings and errors are measured by a DefaultTelemetryProvider
instance. These timings and errors can be exported for visibility by configuring a TelemetryExporter
. The following configuration code enables a consoleTelemetryExporter
which outputs timing and error telemetry to the browser console at the debug
level:
window['_dlo_telemetryExporter'] = 'console';
Tip: To view console telemetry output, ensure your browser console is configured to display verbose/debug logging.
Measured telemetry includes:
- Time elapsed to initialize DLO
- Time elapsed to register all configured rules
- Time elapsed to handle each observed data layer change
- Error counts e.g. due to malformed rules or mismatches between configured rules and observed data layers
Read more about telemetry to learn how to customize telemetry measurement or to export measured telemetry to a custom destination.
See the CHANGELOG for features and fixes.