This doc provides a walkthrough of developing on, and contributing to, Typewriter.
Please see our issue template for issues specifically.
Have an idea for improving Typewriter? Submit an issue first, and we'll be happy to help you scope it out and make sure it is a good fit for Typewriter.
Before working towards adding a new language target, please open an issue on GitHub that walks through your proposal for the new language support. See the issue template for details.
When you are ready to start on a PR:
-
Create a
gen-{your-lang-here}.ts
file insrc/commands
. -
Export the following variables to create a new
yargs
command:
command
: the name of the command (gen-js
,gen-go
, etc.).desc
: the description shown intypewriter --help
.builder
: an object that captures the parameters your command takes. In the majority of cases, you can just re-exportbuilder
fromsrc/lib/index.ts
.handler
: a function that accepts user-specifiedparams
and Tracking Planevents
. This is where you can extract type information and other metadata from theevents
and use it compile your Typewriter client, usually with some kind of JSON Schema library (seeImplementing a Typewriter Client Compiler
below).
/*
* gen-go.ts
*/
const genGo = (events: TrackedEvent[]): Promise<string> => {
// Generate your Typewriter client here.
}
export const handler = getTypedTrackHandler(
async (params: Params, { events }) => {
const codeContent = await genGo(events);
return writeFile(`${params.outputPath}/index.go`, codeContent);
}
);
A single promise must be returned from handler
. In order to render multiple files, use a Promise.all()
to concurrently render them to disk:
/*
* gen-node.ts
*/
const readmeContent = `
Welcome to Typewriter Node.js!
`
const genNodeJS = (events: TrackedEvent[], params: Params): Promise<string> => {
// Generate your Typewriter client here.
}
export const handler = getTypedTrackHandler(async (params: Params, { events }) => {
const indexJS = await genNodeJS(events, params)
const packageJSON = `{ ... }`
return Promise.all([
writeFile(`${params.outputPath}/index.js`, indexJS),
writeFile(`${params.outputPath}/package.json`, packageJSON)
writeFile(`${params.outputPath}/README.md`, readmeContent)
])
})
-
Implement your compiler, following the instruction under
Implementing a Typewriter Client Compiler
below. -
Most importantly, make sure to include an example program in the
examples/
directory that shows how to use your client library. You should use the Tracking Plan fromexamples/local-tracking-plans/tracking-plan.json
. -
Update the
tests
directory to generate snapshot tests for your new language. Simply add a test file:commands/gen-{language}/gen-{language}.test.ts
with a snapshot test:
import { genGo } from '../../../src/commands/gen-go'
import { testSnapshotSingleFile } from '../snapshots'
test('genGo - compiled output matches snapshot', async () => {
await testSnapshotSingleFile(events => genGo(events), 'index.go')
})
If you have any questions, feel free to raise them in the issue for your language proposal. We're happy to help out!
For context, it's useful to have some background on JSON Schema. Here are some great resources:
At a high-level, a Typewriter client exposes a series of analytics functions, each of which represents a single track
event. Together, these events form a Tracking Plan and each event has a corresponding JSON Schema. Each of these functions are typed, to provide build-time validation, if the underlying language can support it. All functions should support run-time validation, since portions of JSON Schema cannot be represented as types (such as regex on strings, for example). Both types of validation are generated using some kind of JSON Schema library, with the former usually generated with QuickType
, while the latter is always a language-specific JSON Schema library (AJV.js
, gojsonschema
, etc.).
As an example, TypeScript supports build-time validation through TypeScript declarations (example) which are compiled using QuickType
. Conversely, JavaScript can only support run-time validation, so we use AJV.js
to pre-compile a validation function that executes at run-time (example).
Note: When performing run-time validation, you can either pre-compile the validation functions (like we do with
AJV.js
), or you can inline/output the JSON Schema, and call out to your JSON Schema library (like we will do withgojsonschema
). The latter requires a peer dependency at development/test time on your language's JSON Schema library.
Note: Run-time validation should always respect the
--runtimeValidation
flag so that users can prevent validation issues from throwing errors in production.
Note: For context, the mobile clients don't (yet) perform runtime validation (they perform build-time validation only). Runtime validation is expected of all new languages.
QuickType
is primarily used for building typed clients for JSON, so it generates code for JSON serialization (which we don't need). Generally you can disable this with a Types Only
or Classes Only
flag. You can test out QuickType
in their online editor here by throwing in one of the example JSON Schemas.
The high-level flow for compiling a Typewriter client is as follows:
- If the language supports types, generate types using
QuickType
. Seegen-android.ts
orgen-js > typescript.ts
as an example. - Generate a function for every event in the Tracking Plan (with typed parameters from #1, if possible). Make sure to normalize the event names (such as
Cart Viewed
) into function names (such ascartViewed
). Within each function:- Generate code to perform run-time JSON Schema validation. See
gen-js > library.ts
as an example. - Issue a call to the underlying analytics instance (
analytics.js
,analytics-go
, etc.).
- Generate code to perform run-time JSON Schema validation. See
Since JSON Schema has an extensive spec, we only explicitely support the features that are supported by the Segment Protocols Tracking Plan Editor (and tested in: tests/__fixtures__/*
). We'll be working towards improving support for other JSON Schema features (such as enums
, anyOf/oneOf/....
, etc.) in the near future. However, when adding support for a new language, you'll only need to support the features documented in tests/__fixtures__/*
, at minimum.
# Install dependencies
$ yarn
# Build the Typewriter CLI
$ yarn build
# Test your Typewriter installation by regenerating the JS example.
$ node ./dist/src/index.js gen-js \
--inputPath ./examples/local-tracking-plans/tracking-plan-slothgram.json \
--outputPath ./examples/gen-js/js/analytics/generated
$ yarn test
You can regenerate all example clients by running:
$ yarn run generate-examples
We follow the Angular commit guidelines.
You can deploy a new version to npm
by running:
$ yarn release