bone.recipes



GuidedTrack cheat sheet, tips, tricks, & best practices

Data

Start and finish times

The columns "Time Started (UTC)" and "Time Finished (UTC)" capture start and finish times of users completing a GuidedTrack program. However, not all users complete programs, so think carefully about whether to use one column or the other when filtering user data by datetime!

Events

Page events

NOTE: The events described below are undocumented and unofficial! They may change or disappear entirely without warning! If you use them for something mission-critical, then make sure you test constantly, even after your program has been published!

These events are emitted when pages start and end:

To listen for them, do:

$(window).on("guidedtrack:pageStart", () => {
  // ...
})

$(window).on("guidedtrack:pageEnd", () => {
  // ...
})

In most cases, the two events are triggered in almost immediate succession because GuidedTrack pages typically load all at once. However, there are cases in which a page may not load all at once. For example:

-- "guidedtrack:pageStart" is triggered here

Here's some text.

*wait: 30.seconds

Here's some more text.

*wait: 30.seconds

*question: Why?

-- "guidedtrack:pageEnd" is triggered here ~60 seconds after
-- "guidedtrack:pageStart" was triggered

HTTP requests

Generally, HTTP requests are fairly straightforward. However, I recently discovered an edge case with an unusual solution.

According to Richard, the "Content-Type": "application/json" header is hard-coded into all requests. However, some servers expect requests to include "multipart/form-data" data. The solution seems to be that the data to be sent should be encoded as query string parameters, like this:

>> foo = "FOO!"
>> bar = "BAR!"

*service: Some Service
  *method: POST
  *path: /some/endpoint?foo={foo}&bar={bar}
  *success
    Success!
  *error
    Error!

Although the above worked for my specific use case (which involved calling the Mailgun API), it's not clear if all servers will respond so nicely to requests encoded as "application/json" when what they really want is "multipart/form-data". I tried setting a "Content-Type": "multipart/form-data" header in the program settings, but it didn't seem to make a difference, presumably because (as Richard said) the content type is already hard-coded into the request. I need to investigate this further.

Hacks & workarounds

Setting variable values from JS

window.data.set_variable("myCoolVariable", "someValue")
// the value can be pretty much anything that can be converted into GT types!

Too much recursion

When running a *while loop for a long time, a "too much recursion" error is occasionally thrown to the console, and the program halts. (This is odd because I'd always assumed while loops in any language were iterative. But maybe I'm wrong!) To work around this, insert a meaningless *trigger, like this:

>> i = 0

*while: i < 999999999
  >> i = i + 1
  *trigger: some-nonexistent-event

Force a page break

*html
	<style>
		#countdown { visibility: hidden !important; }
	</style>

*question: {""}
	*countdown: 0.1.seconds
	*classes: hidden

Create boolean values

-- true
*set: some_true_value

-- false
>> some_false_value = "false".decode("JSON")

Work with whitespace characters

Create standard whitespace characters:

-- newline (initially wrapped in double-quotes)
>> newline = "%22\n%22".decode("URL")
>> newline = newline.decode("JSON")
-- ready to use!

Check if a character is whitespace (using the above newline variable):

>> newline_cleaned = newline.clean

*if: newline_cleaned.size = 0
  -- whitespace!

Compare durations

It's not possible to compare durations directly in GT. For example, this won't work:

*if: 30.minutes < 45.minutes
  Yep.

However, datetimes can be compared; so adding durations to datetimes makes them comparable! Like this:

>> this_magic_moment = calendar::now

*if: this_magic_moment + 30.minutes < this_magic_moment + 45.minutes
  Yep.

Miscellaneous

Jump to specific labels

Append *label names to the end of public URLs to jump directly to those labels, like this:

https://www.guidedtrack.com/programs/[program-id]/run#[label-name]

NOTE: For this to work, the label in question must be in the "main" program. This won't work for labels in sub-programs.

Best practices

It's possible that I'm wrong about these "best practices". However, they've worked fairly well for me so far, which is why I'm writing them down here.

Media

Embedding media from hosts like YouTube and Imgur is easy, but it often doesn't work for users for a variety of reasons. Sometimes the media is restricted based on geographic location. Sometimes the creator or host removes it or puts it behind a paywall. Sometimes the host updates its URL scheme and doesn't redirect the old link correctly. And so on. So, in short, even though it takes a bit more work, it's better in the long run to upload the media to a permanent location that you mostly control (e.g., a CDN, a server that you own, etc.) and link to it from there.

Program names & versions

Developers of all kinds of software are familiar with semantic versioning (AKA "semver"), which assigns version numbers to software in the form MAJOR.MINOR.PATCH (e.g., 3.5.2, where MAJOR = 3, MINOR = 5, and PATCH = 2). Going from smallest to largest: patch numbers are incremented whenever bugs are fixed, minor numbers are incremented whenever new backwards-compatible changes are made, and major numbers are incremented whenever new non-backwards-compatible changes are made. Thus a semver number communicates to users of software the extent to which an updated copy of the software is functionally compatible with whatever copy they're currently using.

For example, suppose you use a piece of accounting software for your business, and the version you're currently running is 7.5.8. The developers of the software indicate that there's a new version available for download: version 8.3.2. Should you download and install the new version? Well, the version number 8.3.2 tells you that, compared to 7.5.8, the new software will include some non-backwards-compatible changes, which could potentially negatively impact how you use the software. Therefore, it'll be important to proceed with caution and test the new software thoroughly before adopting it fully. (However, if the new version was 7.5.9, then you could feel fairly confident that installing the new version would only improve the software, since it likely contains only bug fixes without introducing changes that might disrupt the way you use it.)

When regular computer users install operating system or other software updates, they probably rarely check version numbers but instead assume that the newest version must automatically be the best version. And while that may be a safe assumption for the average user, developers tend to be much pickier because they know how fragile and buggy software can be, and they may value stability more than novelty and prefer to install updates only when they can feel confident that doing so won't break anything. Thus, when developers install each other's code (in the form of libraries or packages), the installer tools are often configured so that they will not automatically install any updates for which there are mismatches between incoming major version numbers and already-in-use major version numbers. (The installer tools can still forcibly install such updates manually, though they usually alert the developer that doing so will likely result in breaking changes.)

So, in summary, semantic versioning is one tool that can be used to help consumers of your code and software know when it's safe (or not) to install updated versions of your code or software.

In GuidedTrack, it's extremely common to update your code, share your code with others, and rely on code others have shared with you; and thus the problems described above are no less prevalent in GT than in other programming contexts. Unfortunately, though, GT does not include any mechanisms for enforcing the use of particular program versions. (Each program does keep its own internal version number so that developers can scrub back through the program's history if needed. But those numbers can't be used to "pin" the use of particular versions of the program.) So if developer A relies on code from developer B, then A's programs are prone to end up breaking when B updates their code. Not only that, but A cannot even specify a version of B's code to use; so when B makes a change, A's programs automatically incorporate it!

Since GT doesn't incorporate version pinning, I recommend using semantic versioning on your own GT programs.

Concretely, this can be accomplished via a few easy steps.

1. Include major version numbers in program names. Do not include the full version number in the name, though.

Others call your programs (using *program) by name, like this:

*program: Josh's Cool Program

By default, though, this naming scheme doesn't include any information about the program's version. So, I recommend using the major version number in the program name, like this:

*program: Josh's Cool Program (version 3)
*program: Josh's Cool Program (v3)
-- etc.

However, it's important not to use full version numbers in program names. For example, if your program name included a full version number, like this:

*program: Josh's Cool Program (version 3.5.2)

...and if you fixed a bug and then updated your program's name to "Josh's Cool Program (version 3.5.3)", then any *program calls that reference the previous name would break because the program they're referencing no longer exists from their perspective.

Full semantic version numbers can still be included in the source code of the program, though, perhaps in comments at the beginning of the program.

2. Duplicate the program to update the major version number.

When it's time to move from (e.g.) version 3.5.2 to version 4.0.0, duplicate the program and rename it. If you included only the major version of the program in the name (e.g., "Josh's Cool Program (version 3)"), then you'd just duplicate the program and update the major version number in the name (e.g., "Josh's Cool Program (version 4)").

This way, people who rely on (e.g.) the latest stable 3.x.x version of your program won't have to worry about their programs breaking when you introduce non-backwards-compatible changes in 4.0.0. They can switch to version 4 if they like, but they won't have to. When / if they want to switch, all they'll have to do is change this:

*program: Josh's Cool Program (version 3)

...to this:

*program: Josh's Cool Program (version 4)

Services names

Service names can be tricky because services are defined only in the root-level program, not in sub-programs. If two separate sub-programs rely on a service they've both called "Airtable", but both require separate API keys, URLs, headers, or other service settings, then there'll inevitably be a conflict when the program tries to execute *service: Airtable. I've discovered, for example, that in Clearer Thinking programs — which often rely on many sub-programs — the list of service names will end up including variations on the same name, like "airtable", "Airtable", "Airtable for X", etc., because the sub-programs have each defined the service name in their own way.

Similarly, when a sub-program is removed (i.e., no longer called with *program), it's often hard to figure out which, if any, of the listed services were associated with it. Thus the list of services tends to grow but never shrink.

My solution to these problems is this: Use the program name in the service name.

For example, if your program is called "Josh's Cool Program (version 3)" and uses a *service call, then the service in question should be called something like "Airtable for 'Josh's Cool Program (version 3)'". In other words:

-- Josh's Cool Program (version 3)

*service: Airtable for "Josh's Cool Program (version 3)"
  *path: /foo/bar
  *success
    {""}
  *error
    {""}

This naming scheme, while perhaps a bit ugly to look at, has two advantages:

  1. It helps to prevent service name collisions.
  2. It makes it easy for consumers of your program to know which services are associated with your program.

Alternatively, if you don't like long service names that include program names, you could include the program's ID (i.e., the same ID used to embed the program in an HTML page) in the service name instead, like "Airtable for a1b2c3d".