Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tracking plan v2 api temp change #19

Merged
merged 10 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 106 additions & 18 deletions src/cli/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,70 @@ import { APIError } from '../types';
export namespace RudderAPI {
export type GetTrackingPlanResponse = TrackingPlan;

export type GetTrackingPlanEventsResponse = TrackingPlanEvents;

export type GetTrackingPlanEventsRulesResponse = {
name: string;
description?: string;
rules: JSONSchema7;
};

export type ListTrackingPlansResponse = {
tracking_plans: TrackingPlan[];
};

export type ListTrackingPlansResponseV2 = {
trackingPlans: TrackingPlan[];
};

export type TrackingPlan = {
name: string;
display_name: string;
version: string;
id: string;
rules: {
events: RuleMetadata[];
global: RuleMetadata;
identify_traits: RuleMetadata;
group_traits: RuleMetadata;
global?: RuleMetadata;
identify_traits?: RuleMetadata;
group_traits?: RuleMetadata;
};
create_time: Date;
update_time: Date;
createdAt: Date;
updatedAt: Date;
creationType: string;
workspaceId: string;
};

export type TrackingPlanEvents = {
data: {
id: string;
name: string;
description: string;
eventType: string;
categoryId: string;
workspaceId: string;
createdBy: string;
updatedBy: string;
createdAt: Date;
updatedAt: Date;
identitySection: string;
additionalProperties: boolean;
}[];
};

export type RuleMetadata = {
name: string;
description?: string;
rules: JSONSchema7;
version: number;
};

export type ListWorkspacesResponse = Workspace;

export type Workspace = {
name: string;
display_name: string;
id: string;
create_time: Date;
createdAt: Date;
};
}

Expand All @@ -49,17 +81,55 @@ export async function fetchTrackingPlan(options: {
id: string;
token: string;
email: string;
APIVersion: string;
}): Promise<RudderAPI.TrackingPlan> {
const url = `trackingplans/${options.id}`;
const url =
options.APIVersion === 'v1' ? `trackingplans/${options.id}` : `tracking-plans/${options.id}`;
const response = await apiGet<RudderAPI.GetTrackingPlanResponse>(
url,
options.token,
options.email,
);

response.create_time = new Date(response.create_time);
response.update_time = new Date(response.update_time);
if (options.APIVersion === 'v2') {
response.createdAt = new Date(response.createdAt);
response.updatedAt = new Date(response.updatedAt);
} else {
response.create_time = new Date(response.create_time);
response.update_time = new Date(response.update_time);
}

if (options.APIVersion === 'v2') {
const url = `tracking-plans/${options.id}/events`;
const eventsResponse = await apiGet<RudderAPI.GetTrackingPlanEventsResponse>(
url,
options.token,
options.email,
);
if (eventsResponse) {
const eventsRulesResponsePromise = eventsResponse.data
.filter(ev => ev.eventType === 'track')
.map(async ev => {
const url = `tracking-plans/${options.id}/events/${ev.id}`;
const eventsRulesResponse = await apiGet<RudderAPI.GetTrackingPlanEventsRulesResponse>(
url,
options.token,
options.email,
);
return {
name: eventsRulesResponse.name,
description: eventsRulesResponse.description,
rules: eventsRulesResponse.rules,
};
});
const eventsRulesResponse: RudderAPI.RuleMetadata[] = await Promise.all(
eventsRulesResponsePromise,
);
response.rules = {
events: eventsRulesResponse,
};
}
}
return sanitizeTrackingPlan(response);
}

Expand All @@ -69,17 +139,29 @@ export async function fetchTrackingPlans(options: {
token: string;
email: string;
}): Promise<RudderAPI.TrackingPlan[]> {
const url = 'trackingplans';
const response = await apiGet<RudderAPI.ListTrackingPlansResponse>(
url,
'trackingplans',
options.token,
options.email,
);
response.tracking_plans.map(tp => ({
...tp,
createdAt: new Date(tp.create_time),
updatedAt: new Date(tp.update_time),
}));

const responseV2 = await apiGet<RudderAPI.ListTrackingPlansResponseV2>(
'tracking-plans',
options.token,
options.email,
);
return response.tracking_plans.map(tp => ({
responseV2.trackingPlans.map(tp => ({
...tp,
create_time: new Date(tp.create_time),
update_time: new Date(tp.update_time),
createdAt: new Date(tp.createdAt),
updatedAt: new Date(tp.updatedAt),
}));

return response.tracking_plans.concat(responseV2.trackingPlans);
}

// fetchWorkspace lists the workspace found with a given Rudder API token.
Expand All @@ -94,7 +176,7 @@ export async function fetchWorkspace(options: {
);
return {
...resp,
create_time: new Date(resp.create_time),
createdAt: new Date(resp.createdAt),
};
}

Expand Down Expand Up @@ -138,10 +220,16 @@ export async function validateToken(
async function apiGet<Response>(url: string, token: string, email: string): Promise<Response> {
const resp = got(url, {
baseUrl:
url === 'workspace' ? 'https://api.rudderstack.com/v1' : 'https://api.rudderstack.com/v1/dg',
url === 'workspace'
? 'https://api.rudderstack.com/v1'
: url.includes('trackingplans')
? 'https://api.rudderstack.com/v1/dg'
: 'https://api.rudderstack.com/v2/catalog',
headers: {
'User-Agent': `RudderTyper: ${version})`,
Authorization: `Basic ${Buffer.from(email + ':' + token).toString('base64')}`,
authorization:
url === 'workspace' || url.includes('trackingplans')
? `Basic ${Buffer.from(email + ':' + token).toString('base64')}`
: 'Bearer ' + token,
},
json: true,
timeout: 10000, // ms
Expand Down
34 changes: 25 additions & 9 deletions src/cli/api/trackingplans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TrackingPlanConfig, resolveRelativePath, verifyDirectoryExists } from '
import sortKeys from 'sort-keys';
import * as fs from 'fs';
import { promisify } from 'util';
import { flow } from 'lodash';
import { flow, pickBy } from 'lodash';
import stringify from 'json-stable-stringify';

const writeFile = promisify(fs.writeFile);
Expand Down Expand Up @@ -55,7 +55,8 @@ export async function writeTrackingPlan(
export function sanitizeTrackingPlan(plan: RudderAPI.TrackingPlan): RudderAPI.TrackingPlan {
// TODO: on JSON Schema Draft-04, required fields must have at least one element.
// Therefore, we strip `required: []` from your rules so this error isn't surfaced.
return sortKeys(plan, { deep: true });
const cleanupPlan = pickBy(plan, v => v !== null);
return sortKeys(cleanupPlan, { deep: true });
}

export type TrackingPlanDeltas = {
Expand Down Expand Up @@ -107,7 +108,9 @@ export function computeDelta(
return deltas;
}

export function parseTrackingPlanName(name: string): { id: string; workspaceSlug: string } {
export function parseTrackingPlanName(
name: string,
): { id: string; workspaceSlug: string; APIVersion: string } {
const parts = name.split('/');

// Sane fallback:
Expand All @@ -121,15 +124,28 @@ export function parseTrackingPlanName(name: string): { id: string; workspaceSlug
return {
id,
workspaceSlug,
APIVersion: 'v1',
};
}

export function toTrackingPlanURL(name: string): string {
const { id } = parseTrackingPlanName(name);
return `https://api.rudderstack.com/trackingplans/${id}`;
export function toTrackingPlanURL(trackingPlan: RudderAPI.TrackingPlan): string {
if (!trackingPlan.creationType) {
const { id } = parseTrackingPlanName(trackingPlan.name);
return `https://api.rudderstack.com/trackingplans/${id}`;
}
return `https://api.rudderstack.com/tracking-plans/${trackingPlan.id}`;
akashrpo marked this conversation as resolved.
Show resolved Hide resolved
}

export function toTrackingPlanId(trackingPlan: RudderAPI.TrackingPlan): string {
if (!trackingPlan.creationType) {
const { id } = parseTrackingPlanName(trackingPlan.name);
return id;
}
return trackingPlan.id;
}

export function toTrackingPlanId(name: string): string {
const { id } = parseTrackingPlanName(name);
return id;
export function getTrackingPlanName(
trackingPlan: Pick<RudderAPI.TrackingPlan, 'creationType' | 'display_name' | 'name'>,
): string {
return !trackingPlan.creationType ? trackingPlan.display_name : trackingPlan.name;
}
23 changes: 10 additions & 13 deletions src/cli/commands/build.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { ErrorContext, wrapError, toUnexpectedError, WrappedError, isWrappedErro
import figures from 'figures';
import { Init } from './init';
import { getEmail } from '../config/config';
import { toTrackingPlanId } from '../api/trackingplans';
import { getTrackingPlanName, toTrackingPlanId } from '../api/trackingplans';
import { APIError } from '../types';

const readFile = promisify(fs.readFile);
Expand Down Expand Up @@ -170,6 +170,7 @@ export const UpdatePlanStep: React.FC<UpdatePlanStepProps> = ({
workspaceSlug: trackingPlanConfig.workspaceSlug,
token,
email,
APIVersion: trackingPlanConfig.APIVersion,
});
} catch (error) {
handleError(error as WrappedError);
Expand All @@ -188,7 +189,6 @@ export const UpdatePlanStep: React.FC<UpdatePlanStepProps> = ({
setFailedToFindToken(true);
}
}

newTrackingPlan = newTrackingPlan || previousTrackingPlan;
if (!newTrackingPlan) {
handleFatalError(wrapError('Unable to fetch Tracking Plan from local cache or API'));
Expand All @@ -197,19 +197,16 @@ export const UpdatePlanStep: React.FC<UpdatePlanStepProps> = ({

const { events } = newTrackingPlan.rules;
const trackingPlan: RawTrackingPlan = {
name: newTrackingPlan.display_name,
url: toTrackingPlanURL(newTrackingPlan.name),
id: toTrackingPlanId(newTrackingPlan.name),
name: getTrackingPlanName(newTrackingPlan),
url: toTrackingPlanURL(newTrackingPlan),
id: toTrackingPlanId(newTrackingPlan),
version: newTrackingPlan.version,
path: trackingPlanConfig.path,
trackCalls: events
// RudderTyper doesn't yet support event versioning. For now, we just choose the most recent version.
.filter(e => events.every(e2 => e.name !== e2.name || e.version >= e2.version))
.map<JSONSchema7>(e => ({
...e.rules,
title: e.name,
description: e.description,
})),
trackCalls: events.map<JSONSchema7>(e => ({
...e.rules,
title: e.name,
description: e.description,
})),
};

loadedTrackingPlans.push({
Expand Down
22 changes: 15 additions & 7 deletions src/cli/commands/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Fuse from 'fuse.js';
import { StandardProps, DebugContext } from '../index';
import { ErrorContext, WrappedError, wrapError } from './error';
import { APIError } from '../types';
import { getTrackingPlanName } from '../api/trackingplans';

const readir = promisify(fs.readdir);

Expand Down Expand Up @@ -649,20 +650,23 @@ const TrackingPlanPrompt: React.FC<TrackingPlanPromptProps> = ({
}, []);

const onSelect = (item: Item) => {
const trackingPlan = trackingPlans.find(tp => tp.name === item.value)!;
const trackingPlan = trackingPlans.find(tp => getTrackingPlanName(tp) === item.value)!;
onSubmit(trackingPlan);
};

// Sort the Tracking Plan alphabetically by display name.
const choices = orderBy(
trackingPlans.map(tp => ({
label: tp.display_name,
value: tp.name,
label: getTrackingPlanName(tp),
value: getTrackingPlanName(tp),
})),
'label',

'asc',
);
let initialIndex = choices.findIndex(c => !!trackingPlan && c.value === trackingPlan.name);
let initialIndex = choices.findIndex(
c => !!trackingPlan && c.value === getTrackingPlanName(trackingPlan),
);
initialIndex = initialIndex === -1 ? 0 : initialIndex;

const tips = [
Expand Down Expand Up @@ -733,16 +737,19 @@ const SummaryPrompt: React.FC<SummaryPromptProps> = ({
client.moduleTarget = 'CommonJS';
client.scriptTarget = 'ES5';
}
const tp = parseTrackingPlanName(trackingPlan.name);
const tp = trackingPlan.creationType
? { id: trackingPlan.id, workspaceSlug: trackingPlan.workspaceId, APIVersion: 'v2' }
: parseTrackingPlanName(trackingPlan.name);
try {
const config: Config = {
client,
trackingPlans: [
{
name: trackingPlan.display_name,
name: getTrackingPlanName(trackingPlan),
id: tp.id,
workspaceSlug: tp.workspaceSlug,
path,
APIVersion: tp.APIVersion,
},
],
};
Expand Down Expand Up @@ -770,9 +777,10 @@ const SummaryPrompt: React.FC<SummaryPromptProps> = ({
{ label: 'Language', value: language },
{ label: 'Directory', value: path },
{ label: 'API Token', value: `${workspace.name} (${token.slice(0, 10)}...)` },
{ label: 'API Version', value: trackingPlan.creationType ? 'v2' : 'v1' },
{
label: 'Tracking Plan',
value: <Link url={toTrackingPlanURL(trackingPlan.name)}>{trackingPlan.display_name}</Link>,
value: <Link url={toTrackingPlanURL(trackingPlan)}>{getTrackingPlanName(trackingPlan)}</Link>,
},
];

Expand Down
2 changes: 1 addition & 1 deletion src/cli/config/ruddertyper.yml.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ trackingPlans:
- id: {{id}}
workspaceSlug: {{workspaceSlug}}
path: {{path}}

APIVersion: {{APIVersion}}
{{/each}}
Loading
Loading