Skip to content
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
143 changes: 143 additions & 0 deletions .github/workflows/check-analytics-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Check deployed analytics contract

# periodically/manual -> download newest deployed analytics contract -> verify it ->
# generate pilot -> type-check -> notify success/failure -> change nothing.
#
# more details in /docs/readme/typed-analytics-contracts.md

on:
schedule:
# Hourly from 06:00 through 22:00 UTC on weekdays.
- cron: '0 6-22 * * 1-5'
workflow_dispatch:
inputs:
release-tag:
description: Optional exact analytics contract release tag
required: false
type: string

permissions:
contents: read
id-token: write

concurrency:
group: check-deployed-analytics-contract
cancel-in-progress: true

jobs:
verify-mobile-contract:
name: Verify latest deployed Mobile contract
environment: default-branch
runs-on: ubuntu-latest
timeout-minutes: 40
outputs:
release-tag: ${{ steps.shared-contract.outputs.release-tag }}
release-url: ${{ steps.shared-contract.outputs.release-url }}
steps:
- name: Checkout Mobile
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: yarn

- name: Install dependencies
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable

- name: Download contract with shared action
id: shared-contract
uses: MetaMask/github-tools/.github/actions/download-analytics-contract@v1
with:
token-exchange-url: ${{ vars.TOKEN_EXCHANGE_URL }}
target-repository: Consensys/segment-schema
asset-name: metamask-mobile.json
release-tag: ${{ inputs.release-tag }}
output-directory: ${{ runner.temp }}/analytics-contract

- name: Check downloaded analytics contract
id: check
env:
SHARED_RELEASE_TAG: ${{ steps.shared-contract.outputs.release-tag }}
SHARED_RELEASE_URL: ${{ steps.shared-contract.outputs.release-url }}
SHARED_MANIFEST_PATH: ${{ steps.shared-contract.outputs.manifest-path }}
SHARED_ASSET_PATH: ${{ steps.shared-contract.outputs.asset-path }}
SHARED_ASSET_DIGEST: ${{ steps.shared-contract.outputs.asset-digest }}
JSON_OUTPUT: ${{ runner.temp }}/analytics-contract-check.json
GENERATED_OUTPUT_DIRECTORY: ${{ runner.temp }}/analytics-contract-check
shell: bash
run: |
set -euo pipefail
yarn analytics:contract:check \
--manifest "${SHARED_MANIFEST_PATH}" \
--contract "${SHARED_ASSET_PATH}" \
--asset-sha256 "${SHARED_ASSET_DIGEST}" \
--release-tag "${SHARED_RELEASE_TAG}" \
--release-url "${SHARED_RELEASE_URL}" \
--output-directory "${GENERATED_OUTPUT_DIRECTORY}" \
--json-output "${JSON_OUTPUT}"
diff -u \
app/util/analytics/generated/QuickBuyAmountSelected.ts \
"${GENERATED_OUTPUT_DIRECTORY}/generated/QuickBuyAmountSelected.ts"
diff -u \
app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts \
"${GENERATED_OUTPUT_DIRECTORY}/generated/QuickBuyAmountSelected.test-d.ts"
echo "release-tag=$(jq -r '.release.tag' "${JSON_OUTPUT}")" >> "${GITHUB_OUTPUT}"
echo "release-url=$(jq -r '.release.url' "${JSON_OUTPUT}")" >> "${GITHUB_OUTPUT}"

- name: Summarize verification
if: always()
env:
RELEASE_TAG: ${{ steps.shared-contract.outputs.release-tag }}
RELEASE_URL: ${{ steps.shared-contract.outputs.release-url }}
CHECK_RESULT: ${{ steps.check.outcome }}
CHECK_FILE: ${{ runner.temp }}/analytics-contract-check.json
shell: bash
run: |
{
echo "### Deployed Mobile analytics contract"
echo
if [ -n "${RELEASE_TAG}" ]; then
echo "- Release: [\`${RELEASE_TAG}\`](${RELEASE_URL})"
else
echo "- Release: unresolved"
fi
echo "- Contract, generation, and type-check result: \`${CHECK_RESULT}\`"

if [ -f "${CHECK_FILE}" ]; then
echo "- Source SHA: \`$(jq -r '.verification.sourceSha' "${CHECK_FILE}")\`"
echo "- Rule count: \`$(jq -r '.verification.ruleCount' "${CHECK_FILE}")\`"
echo "- Contract hash: \`$(jq -r '.verification.contractHash' "${CHECK_FILE}")\`"
echo "- Checked event versions: \`$(jq -r '.generated.versions | join(", ")' "${CHECK_FILE}")\`"
fi
} >> "${GITHUB_STEP_SUMMARY}"

notify-slack:
name: Notify Slack
needs: verify-mobile-contract
if: >-
${{
always() &&
!cancelled()
}}
environment: default-branch
runs-on: ubuntu-latest
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_PLAYWRIGHT_HEALTH_REPORT }}
Comment thread
NicolasMassart marked this conversation as resolved.
steps:
- name: Send Slack notification
if: env.SLACK_WEBHOOK != ''
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a
with:
webhook: ${{ env.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "Mobile deployed analytics contract check: ${{ needs.verify-mobile-contract.result }}\nRelease: ${{ needs.verify-mobile-contract.outputs.release-tag || 'unresolved' }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
41 changes: 41 additions & 0 deletions app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createQuickBuyAmountSelected } from './QuickBuyAmountSelected';

declare const transport: Parameters<typeof createQuickBuyAmountSelected>[0];

const event = createQuickBuyAmountSelected(transport);

event.v1.track({
amount_selection_method: 'preset',
amount_usd: 1,
source: 'notification',
});

event.v2.track({
amount_selection_method: 'preset',
amount_usd: 1,
source: 'notification',
});

// @ts-expect-error source is required
event.v2.track({
amount_selection_method: 'custom_input',
amount_usd: 1,
});

event.v2.track({
amount_selection_method: 'preset',
amount_usd: 1,
source: 'notification',
// @ts-expect-error unknown properties are rejected
unknown_property: true,
});

event.v1.track({
// @ts-expect-error slider is not a v1 amount selection method
amount_selection_method: 'slider',
amount_usd: 1,
source: 'notification',
});

// @ts-expect-error only generated versions are available
event.v3.track({});
52 changes: 52 additions & 0 deletions app/util/analytics/generated/QuickBuyAmountSelected.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createQuickBuyAmountSelected } from './QuickBuyAmountSelected';

describe('createQuickBuyAmountSelected', () => {
it('tracks v1 with the event version context', () => {
const transport = {
track: jest.fn(),
};
const event = createQuickBuyAmountSelected(transport);
const properties = {
amount_selection_method: 'preset' as const,
amount_usd: 50,
source: 'notification' as const,
};

event.v1.track(properties);

expect(transport.track).toHaveBeenCalledWith(
'Quick Buy Amount Selected',
properties,
{
protocols: {
event_version: 1,
},
},
);
});

it('tracks v2 with the event version context', () => {
const transport = {
track: jest.fn(),
};
const event = createQuickBuyAmountSelected(transport);
const properties = {
amount_selection_method: 'slider' as const,
amount_usd: 50,
slider_percent: 25,
source: 'trader_feed' as const,
};

event.v2.track(properties);

expect(transport.track).toHaveBeenCalledWith(
'Quick Buy Amount Selected',
properties,
{
protocols: {
event_version: 2,
},
},
);
});
});
131 changes: 131 additions & 0 deletions app/util/analytics/generated/QuickBuyAmountSelected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* GENERATED FILE — DO NOT EDIT.
*
* Generated from the verified deployed Mobile analytics contract.
*/

export interface AnalyticsEventContext {
readonly protocols: {
readonly event_version: number;
};
}

export interface AnalyticsEventTransport {
track(
eventName: string,
properties: object,
context: AnalyticsEventContext,
): void;
}

type ExactProperties<Expected, Actual extends Expected> = Actual &
Record<Exclude<keyof Actual, keyof Expected>, never>;

interface EventVersion<Properties> {
track<Actual extends Properties>(
properties: ExactProperties<Properties, Actual>,
): void;
}

const createEventVersion = <Properties>(
transport: AnalyticsEventTransport,
eventName: string,
version: number,
): EventVersion<Properties> => ({
track: (properties) => {
transport.track(eventName, properties as object, {
protocols: {
event_version: version,
},
});
},
});

const EVENT_NAME = 'Quick Buy Amount Selected';

export interface QuickBuyAmountSelectedV1Properties {
/** How the user picked the amount. */
readonly amount_selection_method: 'preset' | 'custom_input';
/** Selected USD amount. */
readonly amount_usd: number;
/** CAIP-19 identifier of the token associated with the event. */
readonly caip19?: string;
/** Symbol of the source token currently selected to pay with (may be undefined before token resolution finishes). */
readonly pay_with_token?: string;
/** Preset chip value, when amount_selection_method is preset. */
readonly preset_value?: 20 | 50 | 100 | 250;
/** Surface from which the QuickBuy sheet was opened. */
readonly source:
| 'notification'
| 'profile_position'
| 'leaderboard'
| 'asset_details'
| 'market_insights'
| 'security_trust'
| 'explore_search'
| 'explore_crypto'
| 'explore_now'
| 'explore_rwas'
| 'explore_trending'
| 'explore_stocks';
/** Wallet address of the trader. May be absent on cold-start notification deeplinks where only a profile UUID is available. */
readonly trader_address?: string;
}

export interface QuickBuyAmountSelectedV2Properties {
/** How the user picked the amount. */
readonly amount_selection_method: 'preset' | 'custom_input' | 'slider';
/** Selected USD amount. */
readonly amount_usd: number;
/** CAIP-19 identifier of the spot token associated with the event, including chain ID. Sent for spot positions only; absent for perps. */
readonly caip19?: string;
/** Lowercase chain slug for the position. Spot examples: base, ethereum, solana. Perps: hyperliquid. Present on follow-trading token events when a position context is resolved. */
readonly chain_name?: string;
/** How the user reached the follow-trading token (trade) screen before opening Quick Buy. Only set when Quick Buy is opened from the trade screen. */
readonly original_entry_point?:
| 'leaderboard'
| 'trader_profile'
| 'notification'
| 'deep_link'
| 'home_carousel';
/** Symbol of the source token currently selected to pay with (may be undefined before token resolution finishes). */
readonly pay_with_token?: string;
/** Tradable Hyperliquid perps market symbol. Sent for perp positions only; absent for spot. */
readonly perps_market?: string;
/** Preset chip value, when amount_selection_method is preset. */
readonly preset_value?: 10 | 50 | 100 | 250;
/** Slider position (0–100), when amount_selection_method is slider. */
readonly slider_percent?: number;
/** Surface from which the QuickBuy sheet was opened. */
readonly source:
| 'notification'
| 'profile_position'
| 'leaderboard'
| 'trader_feed'
| 'asset_details'
| 'market_insights'
| 'security_trust'
| 'explore_search'
| 'explore_crypto'
| 'explore_now'
| 'explore_rwas'
| 'explore_trending'
| 'explore_stocks';
/** Wallet address of the trader. May be absent on cold-start notification deeplinks where only a profile UUID is available. */
readonly trader_address?: string;
}

export const createQuickBuyAmountSelected = (
transport: AnalyticsEventTransport,
) => ({
v1: createEventVersion<QuickBuyAmountSelectedV1Properties>(
transport,
EVENT_NAME,
1,
),
v2: createEventVersion<QuickBuyAmountSelectedV2Properties>(
transport,
EVENT_NAME,
2,
),
});
Loading
Loading