mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-21 21:23:06 -05:00
Analytics (#228)
* metricts-sentry * fixes * refactor * mixpanel integration * improvements * improvements * fixes * changeset * fixes * fixes * Update .changeset/few-scissors-sleep.md Co-authored-by: Jordi Sala Morales <jordism91@gmail.com> * Update manifest.json Co-authored-by: Jordi Sala Morales <jordism91@gmail.com> * Update vite.config.ts Co-authored-by: Jordi Sala Morales <jordism91@gmail.com> * fixes * fixes * fixes * lint --------- Co-authored-by: Jordi Sala Morales <jordism91@gmail.com>
This commit is contained in:
parent
d7426e6578
commit
a079f168df
20 changed files with 1245 additions and 815 deletions
5
.changeset/few-scissors-sleep.md
Normal file
5
.changeset/few-scissors-sleep.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'penpot-exporter': minor
|
||||
---
|
||||
|
||||
Added basic analytics and error tracking using MixPanel and Sentry
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
ui-src/.env
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<a href="https://penpot.app/"><b>Penpot Website</b></a> •
|
||||
<a href="https://community.penpot.app/t/figma-to-penpot-export-plugin/5554"><b>Export Figma to Penpot (Penpot community)</b></a> •
|
||||
<a href="https://community.penpot.app/"><b>Penpot Community</b></a> •
|
||||
<a href="https://www.figma.com/community/plugin/1219369440655168734/penpot-exporter"><b>Plugin in Figma community</b></a>
|
||||
<a href="https://www.figma.com/community/plugin/1219369440655168734/penpot-exporter"><b>Plugin in Figma community</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
@ -96,6 +96,10 @@ then selecting "Download ZIP". Extract the ZIP file to a location on your comput
|
|||
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
||||
dependencies, and then the `npm run build` command to build the plugin.
|
||||
|
||||
#### Building for production:
|
||||
|
||||
Follow the same steps as above, but instead of running `npm run build`, run `npm run build:prod`.
|
||||
|
||||
### Add to Figma
|
||||
|
||||
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…` To add the plugin to
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
"main": "dist/code.js",
|
||||
"ui": "dist/index.html",
|
||||
"editorType": ["figma"],
|
||||
"networkAccess": { "allowedDomains": ["none"] },
|
||||
"networkAccess": {
|
||||
"allowedDomains": [
|
||||
"https://o4508183201316864.ingest.de.sentry.io",
|
||||
"https://api-js.mixpanel.com"
|
||||
],
|
||||
"reasoning": "We use Sentry and Mixpanel to monitor the performance of the plugin and get information about errors to continue improving the experience."
|
||||
},
|
||||
"permissions": ["currentuser"],
|
||||
"documentAccess": "dynamic-page"
|
||||
}
|
||||
|
|
1895
package-lock.json
generated
1895
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,8 @@
|
|||
"description": "Penpot exporter",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui'",
|
||||
"build": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui -- --mode development'",
|
||||
"build:prod": "concurrently -n widget,iframe 'npm:build:main' 'npm:build:ui -- --mode production'",
|
||||
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es2016 --minify",
|
||||
"build:ui": "vite build",
|
||||
"build:watch": "concurrently -n widget,iframe 'npm:build:main -- --watch' 'npm:build:ui -- --watch'",
|
||||
|
@ -23,9 +24,12 @@
|
|||
"license": "MPL2.0",
|
||||
"dependencies": {
|
||||
"@create-figma-plugin/ui": "^3.2",
|
||||
"@sentry/react": "^8.34",
|
||||
"@sentry/vite-plugin": "^2.22",
|
||||
"base64-js": "^1.5",
|
||||
"classnames": "^2.5",
|
||||
"lru-cache": "^11.0",
|
||||
"mixpanel-figma": "^2.0",
|
||||
"preact": "^10.23",
|
||||
"react-hook-form": "^7.52",
|
||||
"romans": "^2.0",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { getUserData } from '@plugin/getUserData';
|
||||
|
||||
import { findAllTextNodes } from './findAllTextnodes';
|
||||
import { handleExportMessage } from './handleExportMessage';
|
||||
import { registerChange } from './registerChange';
|
||||
|
@ -9,6 +11,7 @@ figma.showUI(__html__, { themeColors: true, width: BASE_WIDTH, height: BASE_HEIG
|
|||
|
||||
figma.ui.onmessage = message => {
|
||||
if (message.type === 'ready') {
|
||||
getUserData();
|
||||
findAllTextNodes();
|
||||
}
|
||||
|
||||
|
|
13
plugin-src/getUserData.ts
Normal file
13
plugin-src/getUserData.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const getUserData = async () => {
|
||||
const user = figma.currentUser;
|
||||
if (user) {
|
||||
figma.ui.postMessage({
|
||||
type: 'USER_DATA',
|
||||
data: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Could not get user data');
|
||||
}
|
||||
};
|
|
@ -8,6 +8,6 @@ export const registerComponentProperties = (node: ComponentSetNode | ComponentNo
|
|||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering component properties', node, error);
|
||||
console.warn('Could not register component properties', node, error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ export const transformVectorPaths = (node: VectorNode): PathShape[] => {
|
|||
try {
|
||||
regions = node.vectorNetwork?.regions ?? [];
|
||||
} catch (error) {
|
||||
console.error('Error accessing vector network', node, error);
|
||||
console.warn('Could not access the vector network', node, error);
|
||||
}
|
||||
|
||||
const strokeLength = node.strokes.length;
|
||||
|
|
|
@ -62,7 +62,7 @@ export const transformSceneNode = async (node: SceneNode): Promise<PenpotNode |
|
|||
}
|
||||
|
||||
if (penpotNode === undefined) {
|
||||
console.error(`Unsupported node type: ${node.type}`);
|
||||
console.warn(`Unsupported node type: ${node.type}`);
|
||||
}
|
||||
|
||||
return penpotNode;
|
||||
|
|
|
@ -20,7 +20,7 @@ export const translateFill = (fill: Paint): Fill | undefined => {
|
|||
return translateImageFill(fill);
|
||||
}
|
||||
|
||||
console.error(`Unsupported fill type: ${fill.type}`);
|
||||
console.warn(`Unsupported fill type: ${fill.type}`);
|
||||
};
|
||||
|
||||
export const translateFills = (
|
||||
|
@ -60,5 +60,5 @@ export const translatePageFill = (fill: Paint): string | undefined => {
|
|||
return rgbToHex(fill.color);
|
||||
}
|
||||
|
||||
console.error(`Unsupported page fill type: ${fill.type}`);
|
||||
console.warn(`Unsupported page fill type: ${fill.type}`);
|
||||
};
|
||||
|
|
2
ui-src/.env.example
Normal file
2
ui-src/.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
VITE_SENTRY_DSN=
|
||||
VITE_MIXPANEL_TOKEN=
|
|
@ -12,7 +12,8 @@ type PluginMessage =
|
|||
| ProgressCurrentItemMessage
|
||||
| ProgressTotalItemsMessage
|
||||
| ProgressProcessedItemsMessage
|
||||
| ErrorMessage;
|
||||
| ErrorMessage
|
||||
| UserDataMessage;
|
||||
|
||||
type PenpotDocumentMessage = {
|
||||
type: 'PENPOT_DOCUMENT';
|
||||
|
@ -53,6 +54,13 @@ type ErrorMessage = {
|
|||
data: string;
|
||||
};
|
||||
|
||||
type UserDataMessage = {
|
||||
type: 'USER_DATA';
|
||||
data: {
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const sendMessage = (pluginMessage: PluginMessage) => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent<MessageData>('message', {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { FormValues } from '@ui/components/ExportForm';
|
||||
import { identify, track } from '@ui/metrics/mixpanel';
|
||||
import { parse } from '@ui/parser';
|
||||
|
||||
import { MessageData, sendMessage } from '.';
|
||||
|
@ -56,6 +57,11 @@ export const useFigma = (): UseFigmaHook => {
|
|||
const { pluginMessage } = event.data;
|
||||
|
||||
switch (pluginMessage.type) {
|
||||
case 'USER_DATA': {
|
||||
identify({ userId: pluginMessage.data.userId });
|
||||
track('Plugin Loaded');
|
||||
break;
|
||||
}
|
||||
case 'PENPOT_DOCUMENT': {
|
||||
const file = await parse(pluginMessage.data);
|
||||
|
||||
|
@ -73,6 +79,10 @@ export const useFigma = (): UseFigmaHook => {
|
|||
|
||||
if (blob) {
|
||||
download(blob, `${pluginMessage.data.name}.zip`);
|
||||
|
||||
// get size of the file in Mb rounded to 2 decimal places
|
||||
const size = Math.round((blob.size / 1024 / 1024) * 100) / 100;
|
||||
track('File Exported', { 'Exported File Size': size + ' Mb' });
|
||||
}
|
||||
|
||||
setExporting(false);
|
||||
|
@ -111,7 +121,8 @@ export const useFigma = (): UseFigmaHook => {
|
|||
setError(true);
|
||||
setLoading(false);
|
||||
setExporting(false);
|
||||
break;
|
||||
track('Error', { 'Error Message': pluginMessage.data });
|
||||
throw new Error(pluginMessage.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,10 +2,16 @@ import 'node_modules/@create-figma-plugin/ui/lib/css/base.css';
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { initializeMixpanel } from '@ui/metrics/mixpanel';
|
||||
import { initializeSentry } from '@ui/metrics/sentry';
|
||||
|
||||
import { App } from './App';
|
||||
import './main.css';
|
||||
import './reset.css';
|
||||
|
||||
initializeMixpanel();
|
||||
initializeSentry();
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
|
34
ui-src/metrics/mixpanel.ts
Normal file
34
ui-src/metrics/mixpanel.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import mixpanel from 'mixpanel-figma';
|
||||
|
||||
export const track = (name: string, opts = {}) => {
|
||||
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||
opts = {
|
||||
...opts,
|
||||
'Plugin Version': APP_VERSION
|
||||
};
|
||||
mixpanel.track(name, opts);
|
||||
}
|
||||
};
|
||||
|
||||
export const identify = ({ userId }: { userId: string }) => {
|
||||
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||
mixpanel.identify(userId);
|
||||
|
||||
mixpanel.people.set({
|
||||
'USER_ID': userId,
|
||||
'Plugin Version': APP_VERSION
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeMixpanel = () => {
|
||||
if (import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD) {
|
||||
mixpanel.init(import.meta.env.VITE_MIXPANEL_TOKEN, {
|
||||
disable_cookie: true,
|
||||
disable_persistence: true,
|
||||
opt_out_tracking_by_default: true,
|
||||
ip: false,
|
||||
track_pageview: true
|
||||
});
|
||||
}
|
||||
};
|
16
ui-src/metrics/sentry.ts
Normal file
16
ui-src/metrics/sentry.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import * as Sentry from '@sentry/react';
|
||||
|
||||
export const initializeSentry = () => {
|
||||
if (import.meta.env.VITE_SENTRY_DSN && import.meta.env.PROD) {
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
release: `penpot-exporter@${APP_VERSION}`,
|
||||
// Tracing
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0 // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
});
|
||||
}
|
||||
};
|
8
ui-src/vite-env.d.ts
vendored
8
ui-src/vite-env.d.ts
vendored
|
@ -1,2 +1,10 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
declare module ViteEnv {
|
||||
interface ImportMetaEnv {
|
||||
VITE_SENTRY_DSN: string;
|
||||
VITE_MIXPANEL_TOKEN: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const APP_VERSION: string;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import * as process from 'node:process';
|
||||
import { defineConfig } from 'vite';
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
@ -6,7 +8,16 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|||
|
||||
export default defineConfig({
|
||||
root: './ui-src',
|
||||
plugins: [svgr(), react(), viteSingleFile({ removeViteModuleLoader: true }), tsconfigPaths()],
|
||||
plugins: [
|
||||
svgr(),
|
||||
react(),
|
||||
viteSingleFile({ removeViteModuleLoader: true }),
|
||||
tsconfigPaths(),
|
||||
sentryVitePlugin({
|
||||
org: 'runroom-sl',
|
||||
project: 'penpot-exporter'
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'react': 'preact/compat',
|
||||
|
@ -19,8 +30,13 @@ export default defineConfig({
|
|||
target: 'esnext',
|
||||
reportCompressedSize: false,
|
||||
outDir: '../dist',
|
||||
|
||||
rollupOptions: {
|
||||
external: ['!../css/base.css']
|
||||
}
|
||||
},
|
||||
sourcemap: true
|
||||
},
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue