Skip to content

Latest commit

 

History

History
156 lines (106 loc) · 8.64 KB

CONTRIBUTING.md

File metadata and controls

156 lines (106 loc) · 8.64 KB

Thanks for taking the time to contribute to Typewriter!

This doc provides a walkthrough of developing on, and contributing to, Typewriter.

Please see our issue template for issues specifically.

Issues, Bugfixes and New Language Support

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.

Developing on Typewriter

Adding a New Language Target

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:

  1. Create a gen-{your-lang-here}.ts file in src/commands.

  2. 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 in typewriter --help.
  • builder: an object that captures the parameters your command takes. In the majority of cases, you can just re-export builder from src/lib/index.ts.
  • handler: a function that accepts user-specified params and Tracking Plan events. This is where you can extract type information and other metadata from the events and use it compile your Typewriter client, usually with some kind of JSON Schema library (see Implementing 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)
  ])
})
  1. Implement your compiler, following the instruction under Implementing a Typewriter Client Compiler below.

  2. 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 from examples/local-tracking-plans/tracking-plan.json.

  3. 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!

Implementing a Typewriter Client Compiler

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 with gojsonschema). 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:

  1. If the language supports types, generate types using QuickType. See gen-android.ts or gen-js > typescript.ts as an example.
  2. 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 as cartViewed). Within each function:

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.

Build and run locally

# 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

Running Tests

$ yarn test

Regenerate All Example Clients

You can regenerate all example clients by running:

$ yarn run generate-examples

Conventions

We follow the Angular commit guidelines.

Deploying

You can deploy a new version to npm by running:

$ yarn release