mirror of
https://github.com/strapi/strapi.git
synced 2025-09-19 13:31:34 +00:00
Merge commit 'af0cd00b2862212486d3e6c6fd3ab16e1e6f63ea' of https://github.com/GitStartHQ/strapi into fix/add-button-repeatable-components
This commit is contained in:
commit
f3664d53b4
@ -20,7 +20,7 @@ import {
|
||||
fetchUserRoles,
|
||||
} from './utils/api';
|
||||
import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion';
|
||||
import { getFullName } from '../../utils';
|
||||
import { getFullName, hashAdminUserEmail } from '../../utils';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
@ -31,6 +31,7 @@ const AuthenticatedApp = () => {
|
||||
const userInfo = auth.getUserInfo();
|
||||
const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname);
|
||||
const [userDisplayName, setUserDisplayName] = useState(userName);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const { showReleaseNotification } = useConfigurations();
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
@ -71,6 +72,15 @@ const AuthenticatedApp = () => {
|
||||
}
|
||||
}, [userRoles, appInfos]);
|
||||
|
||||
useEffect(() => {
|
||||
const getUserId = async () => {
|
||||
const userId = await hashAdminUserEmail(userInfo);
|
||||
setUserId(userId);
|
||||
};
|
||||
|
||||
getUserId();
|
||||
}, [userInfo]);
|
||||
|
||||
// We don't need to wait for the release query to be fetched before rendering the plugins
|
||||
// however, we need the appInfos and the permissions
|
||||
const shouldShowNotDependentQueriesLoader =
|
||||
@ -81,12 +91,13 @@ const AuthenticatedApp = () => {
|
||||
const appInfosValue = useMemo(() => {
|
||||
return {
|
||||
...appInfos,
|
||||
userId,
|
||||
latestStrapiReleaseTag: tag_name,
|
||||
setUserDisplayName,
|
||||
shouldUpdateStrapi,
|
||||
userDisplayName,
|
||||
};
|
||||
}, [appInfos, tag_name, shouldUpdateStrapi, userDisplayName]);
|
||||
}, [appInfos, tag_name, shouldUpdateStrapi, userDisplayName, userId]);
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <LoadingIndicatorPage />;
|
||||
|
@ -20,7 +20,9 @@ const strapiVersion = packageJSON.version;
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
auth: { getUserInfo: () => ({ firstname: 'kai', lastname: 'doe' }) },
|
||||
auth: {
|
||||
getUserInfo: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
|
||||
},
|
||||
useGuidedTour: jest.fn(() => ({
|
||||
setGuidedTourVisibility: jest.fn(),
|
||||
})),
|
||||
|
@ -24,18 +24,13 @@ import { RELATION_ITEM_HEIGHT } from './constants';
|
||||
import { usePrev } from '../../hooks';
|
||||
|
||||
const LinkEllipsis = styled(Link)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inherit;
|
||||
`;
|
||||
display: block;
|
||||
|
||||
const BoxEllipsis = styled(Box)`
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inherit;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -322,7 +317,7 @@ const RelationInput = ({
|
||||
}
|
||||
style={style}
|
||||
>
|
||||
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
|
||||
<Box minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
|
||||
<Tooltip description={mainField ?? `${id}`}>
|
||||
{href ? (
|
||||
<LinkEllipsis to={href} disabled={disabled}>
|
||||
@ -334,7 +329,7 @@ const RelationInput = ({
|
||||
</Typography>
|
||||
)}
|
||||
</Tooltip>
|
||||
</BoxEllipsis>
|
||||
</Box>
|
||||
|
||||
{publicationState && (
|
||||
<Status variant={statusColor} showBullet={false} size="S">
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Content-Manager || RelationInput should render and match snapshot 1`] = `
|
||||
.c36 {
|
||||
.c35 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
@ -49,7 +49,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
.c22 {
|
||||
background: #eaf5ff;
|
||||
padding-top: 4px;
|
||||
padding-right: 8px;
|
||||
@ -60,16 +60,16 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
border: 1px solid #b8e1ff;
|
||||
}
|
||||
|
||||
.c26 {
|
||||
.c25 {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.c28 {
|
||||
.c27 {
|
||||
color: #666687;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.c31 {
|
||||
.c30 {
|
||||
background: #eafbe7;
|
||||
padding-top: 4px;
|
||||
padding-right: 8px;
|
||||
@ -80,7 +80,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
border: 1px solid #c6f0c2;
|
||||
}
|
||||
|
||||
.c34 {
|
||||
.c33 {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@ -165,20 +165,20 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
color: #4945ff;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
.c21 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
.c24 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
font-weight: 600;
|
||||
color: #006096;
|
||||
}
|
||||
|
||||
.c30 {
|
||||
.c29 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
display: block;
|
||||
@ -188,14 +188,14 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
color: #4945ff;
|
||||
}
|
||||
|
||||
.c33 {
|
||||
.c32 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
font-weight: 600;
|
||||
color: #2f6846;
|
||||
}
|
||||
|
||||
.c35 {
|
||||
.c34 {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
color: #666687;
|
||||
@ -210,7 +210,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.c29 path {
|
||||
.c28 path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
@ -300,15 +300,15 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
width: 0.5625rem;
|
||||
}
|
||||
|
||||
.c24 .c5 {
|
||||
.c23 .c5 {
|
||||
color: #0c75af;
|
||||
}
|
||||
|
||||
.c32 .c5 {
|
||||
.c31 .c5 {
|
||||
color: #328048;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
.c19 {
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
@ -324,31 +324,31 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c20 svg path {
|
||||
.c19 svg path {
|
||||
-webkit-transition: fill 150ms ease-out;
|
||||
transition: fill 150ms ease-out;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c20 svg {
|
||||
.c19 svg {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.c20 .c5 {
|
||||
.c19 .c5 {
|
||||
-webkit-transition: color 150ms ease-out;
|
||||
transition: color 150ms ease-out;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.c20:hover {
|
||||
.c19:hover {
|
||||
color: #7b79ff;
|
||||
}
|
||||
|
||||
.c20:active {
|
||||
.c19:active {
|
||||
color: #271fe0;
|
||||
}
|
||||
|
||||
.c20:after {
|
||||
.c19:after {
|
||||
-webkit-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.2s;
|
||||
@ -363,11 +363,11 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.c20:focus-visible {
|
||||
.c19:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c20:focus-visible:after {
|
||||
.c19:focus-visible:after {
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -407,26 +407,23 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
.c20 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.c20 > span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.c19 > span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.c27 svg path {
|
||||
.c26 svg path {
|
||||
fill: #8e8ea9;
|
||||
}
|
||||
|
||||
.c27:hover svg path,
|
||||
.c27:focus svg path {
|
||||
.c26:hover svg path,
|
||||
.c26:focus svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
@ -585,18 +582,18 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
class="c16 c17"
|
||||
>
|
||||
<div
|
||||
class="c18 c19"
|
||||
class="c18"
|
||||
>
|
||||
<span>
|
||||
<a
|
||||
aria-current="page"
|
||||
aria-describedby="tooltip-1"
|
||||
class="c20 c21 active"
|
||||
class="c19 c20 active"
|
||||
href="/"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="c5 c22"
|
||||
class="c5 c21"
|
||||
>
|
||||
Relation 1
|
||||
</span>
|
||||
@ -604,26 +601,26 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c23 c24"
|
||||
class="c22 c23"
|
||||
>
|
||||
<span
|
||||
class="c5 c25"
|
||||
class="c5 c24"
|
||||
>
|
||||
Draft
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c26"
|
||||
class="c25"
|
||||
>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
class="c27"
|
||||
class="c26"
|
||||
data-testid="remove-relation-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="c28 c29"
|
||||
class="c27 c28"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -650,12 +647,12 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
class="c16 c17"
|
||||
>
|
||||
<div
|
||||
class="c18 c19"
|
||||
class="c18"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
aria-describedby="tooltip-3"
|
||||
class="c5 c30"
|
||||
class="c5 c29"
|
||||
tabindex="0"
|
||||
>
|
||||
Relation 2
|
||||
@ -663,26 +660,26 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c31 c32"
|
||||
class="c30 c31"
|
||||
>
|
||||
<span
|
||||
class="c5 c33"
|
||||
class="c5 c32"
|
||||
>
|
||||
Published
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c26"
|
||||
class="c25"
|
||||
>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
class="c27"
|
||||
class="c26"
|
||||
data-testid="remove-relation-2"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="c28 c29"
|
||||
class="c27 c28"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -709,12 +706,12 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
class="c16 c17"
|
||||
>
|
||||
<div
|
||||
class="c18 c19"
|
||||
class="c18"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
aria-describedby="tooltip-5"
|
||||
class="c5 c30"
|
||||
class="c5 c29"
|
||||
tabindex="0"
|
||||
>
|
||||
Relation 3
|
||||
@ -723,16 +720,16 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c26"
|
||||
class="c25"
|
||||
>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
class="c27"
|
||||
class="c26"
|
||||
data-testid="remove-relation-3"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="c28 c29"
|
||||
class="c27 c28"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -753,10 +750,10 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c34"
|
||||
class="c33"
|
||||
>
|
||||
<p
|
||||
class="c5 c35"
|
||||
class="c5 c34"
|
||||
id="1-hint"
|
||||
>
|
||||
this is a description
|
||||
@ -764,7 +761,7 @@ exports[`Content-Manager || RelationInput should render and match snapshot 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c36"
|
||||
class="c35"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
|
@ -35,7 +35,10 @@ function App() {
|
||||
const toggleNotification = useNotification();
|
||||
const { updateProjectSettings } = useConfigurations();
|
||||
const { formatMessage } = useIntl();
|
||||
const [{ isLoading, hasAdmin, uuid }, setState] = useState({ isLoading: true, hasAdmin: false });
|
||||
const [{ isLoading, hasAdmin, uuid, deviceId }, setState] = useState({
|
||||
isLoading: true,
|
||||
hasAdmin: false,
|
||||
});
|
||||
const appInfo = useAppInfos();
|
||||
const { get } = useFetchClient();
|
||||
|
||||
@ -81,6 +84,7 @@ function App() {
|
||||
} = await axios.get(`${strapi.backendURL}/admin/init`);
|
||||
|
||||
updateProjectSettings({ menuLogo: prefixFileUrlWithBackendUrl(menuLogo) });
|
||||
const deviceId = await getUID();
|
||||
|
||||
if (uuid) {
|
||||
const {
|
||||
@ -93,18 +97,16 @@ function App() {
|
||||
setTelemetryProperties(properties);
|
||||
|
||||
try {
|
||||
const deviceId = await getUID();
|
||||
|
||||
await fetch('https://analytics.strapi.io/track', {
|
||||
await fetch('https://analytics.strapi.io/api/v2/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
// This event is anonymous
|
||||
event: 'didInitializeAdministration',
|
||||
uuid,
|
||||
userId: '',
|
||||
deviceId,
|
||||
properties: {
|
||||
...properties,
|
||||
environment: appInfo.currentEnvironment,
|
||||
},
|
||||
eventPropeties: {},
|
||||
userProperties: { environment: appInfo.currentEnvironment },
|
||||
groupProperties: { ...properties, projectId: uuid },
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -115,7 +117,7 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
setState({ isLoading: false, hasAdmin, uuid });
|
||||
setState({ isLoading: false, hasAdmin, uuid, deviceId });
|
||||
} catch (err) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
@ -134,8 +136,9 @@ function App() {
|
||||
() => ({
|
||||
uuid,
|
||||
telemetryProperties,
|
||||
deviceId,
|
||||
}),
|
||||
[uuid, telemetryProperties]
|
||||
[uuid, telemetryProperties, deviceId]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -7,3 +7,4 @@ export { default as sortLinks } from './sortLinks';
|
||||
export { default as getExistingActions } from './getExistingActions';
|
||||
export { default as getRequestUrl } from './getRequestUrl';
|
||||
export { default as getFullName } from './getFullName';
|
||||
export { default as hashAdminUserEmail } from './uniqueAdminHash';
|
||||
|
@ -0,0 +1,49 @@
|
||||
import crypto from 'crypto';
|
||||
import { TextEncoder } from 'util';
|
||||
import hashAdminUserEmail, { utils } from '../uniqueAdminHash';
|
||||
|
||||
const testHashValue = '8544bf5b5389959462912699664f03ed664a4b6d24f03b13bdbc362efc147873';
|
||||
|
||||
describe('Creating admin user email hash in admin', () => {
|
||||
afterAll(() => {
|
||||
Object.defineProperty(global.self, 'crypto', {
|
||||
value: undefined,
|
||||
});
|
||||
Object.defineProperty(global.self, 'TextEncoder', {
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty string if no payload provided', async () => {
|
||||
const testHash = await hashAdminUserEmail();
|
||||
|
||||
expect(testHash).toBe(null);
|
||||
});
|
||||
|
||||
it('should return hash using crypto subtle', async () => {
|
||||
Object.defineProperty(global.self, 'crypto', {
|
||||
value: {
|
||||
subtle: {
|
||||
digest: jest.fn((type, message) => crypto.createHash('sha256').update(message).digest()),
|
||||
},
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(global.self, 'TextEncoder', {
|
||||
value: TextEncoder,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
email: 'testemail@strapi.io',
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(utils, 'digestMessage');
|
||||
|
||||
const testHash = await hashAdminUserEmail(payload);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(testHash).toBe(testHashValue);
|
||||
});
|
||||
});
|
22
packages/core/admin/admin/src/utils/uniqueAdminHash.js
Normal file
22
packages/core/admin/admin/src/utils/uniqueAdminHash.js
Normal file
@ -0,0 +1,22 @@
|
||||
export const utils = {
|
||||
bufferToHex(buffer) {
|
||||
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
},
|
||||
async digestMessage(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
||||
|
||||
return this.bufferToHex(hashBuffer);
|
||||
},
|
||||
};
|
||||
|
||||
export default async function hashAdminUserEmail(payload) {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await utils.digestMessage(payload.email);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -69,6 +69,7 @@ describe('Admin Controller', () => {
|
||||
'info.dependencies': {
|
||||
dependency: '1.0.0',
|
||||
},
|
||||
uuid: 'testuuid',
|
||||
environment: 'development',
|
||||
}[key] || value)
|
||||
),
|
||||
@ -85,12 +86,14 @@ describe('Admin Controller', () => {
|
||||
['autoReload', false],
|
||||
['info.strapi', null],
|
||||
['info.dependencies', {}],
|
||||
['uuid', null],
|
||||
]);
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.data).toStrictEqual({
|
||||
currentEnvironment: 'development',
|
||||
autoReload: false,
|
||||
strapiVersion: '1.0.0',
|
||||
projectId: 'testuuid',
|
||||
dependencies: {
|
||||
dependency: '1.0.0',
|
||||
},
|
||||
|
@ -110,6 +110,7 @@ module.exports = {
|
||||
const autoReload = strapi.config.get('autoReload', false);
|
||||
const strapiVersion = strapi.config.get('info.strapi', null);
|
||||
const dependencies = strapi.config.get('info.dependencies', {});
|
||||
const projectId = strapi.config.get('uuid', null);
|
||||
const nodeVersion = process.version;
|
||||
const communityEdition = !strapi.EE;
|
||||
const useYarn = await exists(path.join(process.cwd(), 'yarn.lock'));
|
||||
@ -120,6 +121,7 @@ module.exports = {
|
||||
autoReload,
|
||||
strapiVersion,
|
||||
dependencies,
|
||||
projectId,
|
||||
nodeVersion,
|
||||
communityEdition,
|
||||
useYarn,
|
||||
|
@ -19,7 +19,12 @@ describe('Metrics', () => {
|
||||
|
||||
await metricsService.sendDidInviteUser();
|
||||
|
||||
expect(send).toHaveBeenCalledWith('didInviteUser', { numberOfRoles: 3, numberOfUsers: 2 });
|
||||
expect(send).toHaveBeenCalledWith('didInviteUser', {
|
||||
groupProperties: {
|
||||
numberOfRoles: 3,
|
||||
numberOfUsers: 2,
|
||||
},
|
||||
});
|
||||
expect(countUsers).toHaveBeenCalledWith();
|
||||
expect(countRoles).toHaveBeenCalledWith();
|
||||
});
|
||||
@ -52,7 +57,9 @@ describe('Metrics', () => {
|
||||
|
||||
expect(getLanguagesInUse).toHaveBeenCalledWith();
|
||||
expect(send).toHaveBeenCalledWith('didChangeInterfaceLanguage', {
|
||||
languagesInUse: ['en', 'fr', 'en'],
|
||||
userProperties: {
|
||||
languagesInUse: ['en', 'fr', 'en'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,9 @@ const { getService } = require('../utils');
|
||||
const sendDidInviteUser = async () => {
|
||||
const numberOfUsers = await getService('user').count();
|
||||
const numberOfRoles = await getService('role').count();
|
||||
strapi.telemetry.send('didInviteUser', { numberOfRoles, numberOfUsers });
|
||||
strapi.telemetry.send('didInviteUser', {
|
||||
groupProperties: { numberOfRoles, numberOfUsers },
|
||||
});
|
||||
};
|
||||
|
||||
const sendDidUpdateRolePermissions = async () => {
|
||||
@ -14,7 +16,8 @@ const sendDidUpdateRolePermissions = async () => {
|
||||
|
||||
const sendDidChangeInterfaceLanguage = async () => {
|
||||
const languagesInUse = await getService('user').getLanguagesInUse();
|
||||
strapi.telemetry.send('didChangeInterfaceLanguage', { languagesInUse });
|
||||
// This event is anonymous
|
||||
strapi.telemetry.send('didChangeInterfaceLanguage', { userProperties: { languagesInUse } });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -85,6 +85,7 @@ describe('Single Types', () => {
|
||||
},
|
||||
user: {
|
||||
id: 1,
|
||||
email: 'someTestEmailString',
|
||||
},
|
||||
};
|
||||
|
||||
@ -180,7 +181,9 @@ describe('Single Types', () => {
|
||||
);
|
||||
|
||||
expect(sendTelemetry).toHaveBeenCalledWith('didCreateFirstContentTypeEntry', {
|
||||
model: modelUid,
|
||||
eventProperties: {
|
||||
model: modelUid,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -85,7 +85,9 @@ module.exports = {
|
||||
ctx.body = await permissionChecker.sanitizeOutput(entity);
|
||||
|
||||
if (totalEntries === 0) {
|
||||
strapi.telemetry.send('didCreateFirstContentTypeEntry', { model });
|
||||
strapi.telemetry.send('didCreateFirstContentTypeEntry', {
|
||||
eventProperties: { model },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -73,7 +73,9 @@ module.exports = {
|
||||
const newEntity = await entityManager.create(sanitizedBody, model, { params: query });
|
||||
ctx.body = await permissionChecker.sanitizeOutput(newEntity);
|
||||
|
||||
await strapi.telemetry.send('didCreateFirstContentTypeEntry', { model });
|
||||
await strapi.telemetry.send('didCreateFirstContentTypeEntry', {
|
||||
eventProperties: { model },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -66,14 +66,15 @@ describe('metrics', () => {
|
||||
global.strapi = { telemetry: { send } };
|
||||
metricsService = metricsServiceLoader({ strapi });
|
||||
const [containsRelationalFields, displayedFields, displayedRelationalFields] = expectedResult;
|
||||
|
||||
await metricsService.sendDidConfigureListView(contentType, { layouts: { list } });
|
||||
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
expect(send).toHaveBeenCalledWith('didConfigureListView', {
|
||||
displayedFields,
|
||||
containsRelationalFields,
|
||||
displayedRelationalFields,
|
||||
eventProperties: {
|
||||
containsRelationalFields,
|
||||
displayedFields,
|
||||
displayedRelationalFields,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -13,11 +13,11 @@ module.exports = ({ strapi }) => {
|
||||
).length;
|
||||
|
||||
const data = {
|
||||
containsRelationalFields: !!displayedRelationalFields,
|
||||
eventProperties: { containsRelationalFields: !!displayedRelationalFields },
|
||||
};
|
||||
|
||||
if (data.containsRelationalFields) {
|
||||
Object.assign(data, {
|
||||
if (data.eventProperties.containsRelationalFields) {
|
||||
Object.assign(data.eventProperties, {
|
||||
displayedFields,
|
||||
displayedRelationalFields,
|
||||
});
|
||||
|
@ -64,15 +64,17 @@ module.exports = {
|
||||
components: body.components,
|
||||
});
|
||||
|
||||
const metricsProperties = {
|
||||
kind: contentType.kind,
|
||||
hasDraftAndPublish: hasDraftAndPublish(contentType.schema),
|
||||
const metricsPayload = {
|
||||
eventProperties: {
|
||||
kind: contentType.kind,
|
||||
hasDraftAndPublish: hasDraftAndPublish(contentType.schema),
|
||||
},
|
||||
};
|
||||
|
||||
if (_.isEmpty(strapi.api)) {
|
||||
await strapi.telemetry.send('didCreateFirstContentType', metricsProperties);
|
||||
await strapi.telemetry.send('didCreateFirstContentType', metricsPayload);
|
||||
} else {
|
||||
await strapi.telemetry.send('didCreateContentType', metricsProperties);
|
||||
await strapi.telemetry.send('didCreateContentType', metricsPayload);
|
||||
}
|
||||
|
||||
setImmediate(() => strapi.reload());
|
||||
@ -80,7 +82,9 @@ module.exports = {
|
||||
ctx.send({ data: { uid: contentType.uid } }, 201);
|
||||
} catch (error) {
|
||||
strapi.log.error(error);
|
||||
await strapi.telemetry.send('didNotCreateContentType', { error: error.message });
|
||||
await strapi.telemetry.send('didNotCreateContentType', {
|
||||
eventProperties: { error: error.message },
|
||||
});
|
||||
ctx.send({ error: error.message }, 400);
|
||||
}
|
||||
},
|
||||
|
@ -198,60 +198,42 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle databases that don't support window function ROW_NUMBER
|
||||
if (!strapi.db.dialect.supportsWindowFunctions()) {
|
||||
await cleanOrderColumnsForOldDatabases({ id, attribute, db, inverseRelIds, transaction: trx });
|
||||
return;
|
||||
}
|
||||
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
const update = [];
|
||||
const updateBinding = [];
|
||||
const select = ['??'];
|
||||
const selectBinding = ['id'];
|
||||
const where = [];
|
||||
const whereBinding = [];
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
update.push('?? = b.src_order');
|
||||
updateBinding.push(orderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
||||
selectBinding.push(joinColumn.name, orderColumnName);
|
||||
where.push('?? = ?');
|
||||
whereBinding.push(joinColumn.name, id);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
update.push('?? = b.inv_order');
|
||||
updateBinding.push(inverseOrderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
||||
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
||||
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
||||
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
||||
}
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
switch (strapi.db.dialect.client) {
|
||||
case 'mysql':
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE
|
||||
?? as a,
|
||||
(
|
||||
SELECT ${select.join(', ')}
|
||||
FROM ??
|
||||
WHERE ${where.join(' OR ')}
|
||||
) AS b
|
||||
SET ${update.join(', ')}
|
||||
WHERE b.id = a.id`,
|
||||
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
|
||||
)
|
||||
.transacting(trx);
|
||||
await cleanOrderColumnsForInnoDB({ id, attribute, db, inverseRelIds, transaction: trx });
|
||||
break;
|
||||
default: {
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
const update = [];
|
||||
const updateBinding = [];
|
||||
const select = ['??'];
|
||||
const selectBinding = ['id'];
|
||||
const where = [];
|
||||
const whereBinding = [];
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
update.push('?? = b.src_order');
|
||||
updateBinding.push(orderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
||||
selectBinding.push(joinColumn.name, orderColumnName);
|
||||
where.push('?? = ?');
|
||||
whereBinding.push(joinColumn.name, id);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
update.push('?? = b.inv_order');
|
||||
updateBinding.push(inverseOrderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
||||
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
||||
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
||||
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
||||
}
|
||||
|
||||
const joinTableName = addSchema(joinTable.name);
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE ?? as a
|
||||
@ -265,24 +247,29 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
||||
[joinTableName, ...updateBinding, ...selectBinding, joinTableName, ...whereBinding]
|
||||
)
|
||||
.transacting(trx);
|
||||
|
||||
/*
|
||||
`UPDATE :joinTable: as a
|
||||
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
||||
FROM (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
||||
FROM :joinTable:
|
||||
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
||||
) AS b
|
||||
WHERE b.id = a.id`,
|
||||
*/
|
||||
}
|
||||
/*
|
||||
`UPDATE :joinTable: as a
|
||||
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
||||
FROM (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
||||
FROM :joinTable:
|
||||
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
||||
) AS b
|
||||
WHERE b.id = a.id`,
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
const cleanOrderColumnsForOldDatabases = async ({
|
||||
/*
|
||||
* Ensure that orders are following a 1, 2, 3 sequence, without gap.
|
||||
* The use of a temporary table instead of a window function makes the query compatible with MySQL 5 and prevents some deadlocks to happen in innoDB databases
|
||||
*/
|
||||
const cleanOrderColumnsForInnoDB = async ({
|
||||
id,
|
||||
attribute,
|
||||
db,
|
||||
@ -319,6 +306,9 @@ const cleanOrderColumnsForOldDatabases = async ({
|
||||
}
|
||||
)
|
||||
.transacting(trx);
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE ?? as a, (SELECT * FROM ??) AS b
|
||||
|
@ -5,22 +5,31 @@ import useAppInfos from '../useAppInfos';
|
||||
|
||||
const useTracking = () => {
|
||||
const trackRef = useRef();
|
||||
const { uuid, telemetryProperties } = useContext(TrackingContext);
|
||||
const { uuid, telemetryProperties, deviceId } = useContext(TrackingContext);
|
||||
const appInfo = useAppInfos();
|
||||
const userId = appInfo?.userId;
|
||||
|
||||
trackRef.current = async (event, properties) => {
|
||||
if (uuid && !window.strapi.telemetryDisabled) {
|
||||
try {
|
||||
await axios.post('https://analytics.strapi.io/track', {
|
||||
event,
|
||||
properties: {
|
||||
...telemetryProperties,
|
||||
...properties,
|
||||
projectType: window.strapi.projectType,
|
||||
environment: appInfo.currentEnvironment,
|
||||
await axios.post(
|
||||
'https://analytics.strapi.io/api/v2/track',
|
||||
{
|
||||
event,
|
||||
userId,
|
||||
deviceId,
|
||||
eventProperties: { ...properties },
|
||||
userProperties: {},
|
||||
groupProperties: {
|
||||
...telemetryProperties,
|
||||
projectId: uuid,
|
||||
projectType: window.strapi.projectType,
|
||||
},
|
||||
},
|
||||
uuid,
|
||||
});
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
// Silent
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ function setup(props) {
|
||||
telemetryProperties: {
|
||||
nestedProperty: true,
|
||||
},
|
||||
deviceId: 'someTestDeviceId',
|
||||
...props,
|
||||
}}
|
||||
>
|
||||
@ -45,21 +46,33 @@ describe('useTracking', () => {
|
||||
test('Call trackUsage() with all attributes', async () => {
|
||||
useAppInfos.mockReturnValue({
|
||||
currentEnvironment: 'testing',
|
||||
userId: 'someTestUserId',
|
||||
});
|
||||
|
||||
const { result } = await setup();
|
||||
|
||||
result.current.trackUsage('event', { trackingProperty: true });
|
||||
|
||||
expect(axios.post).toBeCalledWith(expect.any(String), {
|
||||
event: 'event',
|
||||
uuid: 1,
|
||||
properties: expect.objectContaining({
|
||||
environment: 'testing',
|
||||
nestedProperty: true,
|
||||
trackingProperty: true,
|
||||
}),
|
||||
});
|
||||
expect(axios.post).toBeCalledWith(
|
||||
expect.any(String),
|
||||
{
|
||||
userId: 'someTestUserId',
|
||||
deviceId: 'someTestDeviceId',
|
||||
event: 'event',
|
||||
eventProperties: {
|
||||
trackingProperty: true,
|
||||
},
|
||||
groupProperties: {
|
||||
nestedProperty: true,
|
||||
projectId: 1,
|
||||
projectType: 'Community',
|
||||
},
|
||||
userProperties: {},
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('Do not track if it has been disabled', async () => {
|
||||
|
@ -242,11 +242,14 @@ class Strapi {
|
||||
sendStartupTelemetry() {
|
||||
// Emit started event.
|
||||
// do not await to avoid slower startup
|
||||
// This event is anonymous
|
||||
this.telemetry.send('didStartServer', {
|
||||
database: strapi.config.get('database.connection.client'),
|
||||
plugins: Object.keys(strapi.plugins),
|
||||
// TODO: to add back
|
||||
// providers: this.config.installedProviders,
|
||||
groupProperties: {
|
||||
database: strapi.config.get('database.connection.client'),
|
||||
plugins: Object.keys(strapi.plugins),
|
||||
// TODO: to add back
|
||||
// providers: this.config.installedProviders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -53,12 +53,12 @@ const generateNewPackageJSON = (packageObj) => {
|
||||
|
||||
const sendEvent = async (uuid) => {
|
||||
try {
|
||||
await fetch('https://analytics.strapi.io/track', {
|
||||
await fetch('https://analytics.strapi.io/api/v2/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event: 'didOptInTelemetry',
|
||||
uuid,
|
||||
deviceId: machineID(),
|
||||
groupProperties: { projectId: uuid },
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
@ -28,12 +28,12 @@ const writePackageJSON = async (path, file, spacing) => {
|
||||
|
||||
const sendEvent = async (uuid) => {
|
||||
try {
|
||||
await fetch('https://analytics.strapi.io/track', {
|
||||
await fetch('https://analytics.strapi.io/api/v2/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event: 'didOptOutTelemetry',
|
||||
uuid,
|
||||
deviceId: machineID(),
|
||||
groupProperties: { projectId: uuid },
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { generateAdminUserHash } = require('../admin-user-hash');
|
||||
const createContext = require('../../../../../../../test/helpers/create-context');
|
||||
|
||||
describe('user email hash', () => {
|
||||
test('should create a hash from admin user email', () => {
|
||||
const state = {
|
||||
user: {
|
||||
email: 'testemail@strapi.io',
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createContext({}, { state });
|
||||
|
||||
global.strapi = {
|
||||
requestContext: {
|
||||
get: jest.fn(() => ctx),
|
||||
},
|
||||
};
|
||||
|
||||
const hash = crypto.createHash('sha256').update('testemail@strapi.io').digest('hex');
|
||||
|
||||
const userId = generateAdminUserHash();
|
||||
expect(userId).toBe(hash);
|
||||
});
|
||||
|
||||
test('should return empty string if user is not available on ctx', () => {
|
||||
const ctx = createContext({}, {});
|
||||
|
||||
global.strapi = {
|
||||
requestContext: {
|
||||
get: jest.fn(() => ctx),
|
||||
},
|
||||
};
|
||||
|
||||
const userId = generateAdminUserHash();
|
||||
expect(userId).toBe('');
|
||||
});
|
||||
});
|
@ -29,6 +29,9 @@ describe('metrics', () => {
|
||||
root: process.cwd(),
|
||||
},
|
||||
},
|
||||
requestContext: {
|
||||
get: jest.fn(() => ({})),
|
||||
},
|
||||
});
|
||||
|
||||
metricsInstance.register();
|
||||
@ -60,6 +63,9 @@ describe('metrics', () => {
|
||||
root: process.cwd(),
|
||||
},
|
||||
},
|
||||
requestContext: {
|
||||
get: jest.fn(() => ({})),
|
||||
},
|
||||
});
|
||||
|
||||
metricsInstance.register();
|
||||
@ -89,18 +95,21 @@ describe('metrics', () => {
|
||||
root: process.cwd(),
|
||||
},
|
||||
},
|
||||
requestContext: {
|
||||
get: jest.fn(() => ({})),
|
||||
},
|
||||
});
|
||||
|
||||
send('someEvent');
|
||||
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
expect(fetch.mock.calls[0][0]).toBe('https://analytics.strapi.io/track');
|
||||
expect(fetch.mock.calls[0][0]).toBe('https://analytics.strapi.io/api/v2/track');
|
||||
expect(fetch.mock.calls[0][1].method).toBe('POST');
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toMatchObject({
|
||||
event: 'someEvent',
|
||||
uuid: 'test',
|
||||
properties: {
|
||||
groupProperties: {
|
||||
projectType: 'Community',
|
||||
projectId: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
@ -128,6 +137,9 @@ describe('metrics', () => {
|
||||
root: process.cwd(),
|
||||
},
|
||||
},
|
||||
requestContext: {
|
||||
get: jest.fn(() => ({})),
|
||||
},
|
||||
});
|
||||
|
||||
send('someEvent');
|
||||
|
15
packages/core/strapi/lib/services/metrics/admin-user-hash.js
Normal file
15
packages/core/strapi/lib/services/metrics/admin-user-hash.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const generateAdminUserHash = () => {
|
||||
const ctx = strapi?.requestContext?.get();
|
||||
if (!ctx?.state?.user) {
|
||||
return '';
|
||||
}
|
||||
return crypto.createHash('sha256').update(ctx.state.user.email).digest('hex');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateAdminUserHash,
|
||||
};
|
@ -55,10 +55,12 @@ const createTelemetryInstance = (strapi) => {
|
||||
return sendEvent(
|
||||
'didCheckLicense',
|
||||
{
|
||||
licenseInfo: {
|
||||
...ee.licenseInfo,
|
||||
projectHash: hashProject(strapi),
|
||||
dependencyHash: hashDep(strapi),
|
||||
groupProperties: {
|
||||
licenseInfo: {
|
||||
...ee.licenseInfo,
|
||||
projectHash: hashProject(strapi),
|
||||
dependencyHash: hashDep(strapi),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ const createMiddleware = ({ sendEvent }) => {
|
||||
|
||||
// Send max. 1000 events per day.
|
||||
if (_state.counter < 1000) {
|
||||
sendEvent('didReceiveRequest', { url: ctx.request.url });
|
||||
sendEvent('didReceiveRequest', { eventProperties: { url: ctx.request.url } });
|
||||
|
||||
// Increase counter.
|
||||
_state.counter += 1;
|
||||
|
@ -10,7 +10,7 @@ const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
|
||||
const { env } = require('@strapi/utils');
|
||||
const ee = require('../../utils/ee');
|
||||
const machineID = require('../../utils/machine-id');
|
||||
const stringifyDeep = require('./stringify-deep');
|
||||
const { generateAdminUserHash } = require('./admin-user-hash');
|
||||
|
||||
const defaultQueryOpts = {
|
||||
timeout: 1000,
|
||||
@ -42,41 +42,49 @@ module.exports = (strapi) => {
|
||||
const serverRootPath = strapi.dirs.app.root;
|
||||
const adminRootPath = path.join(strapi.dirs.app.root, 'src', 'admin');
|
||||
|
||||
const anonymousMetadata = {
|
||||
const anonymousUserProperties = {
|
||||
environment: strapi.config.environment,
|
||||
os: os.type(),
|
||||
osPlatform: os.platform(),
|
||||
osArch: os.arch(),
|
||||
osRelease: os.release(),
|
||||
nodeVersion: process.versions.node,
|
||||
};
|
||||
|
||||
const anonymousGroupProperties = {
|
||||
docker: process.env.DOCKER || isDocker(),
|
||||
isCI: ciEnv.isCI,
|
||||
version: strapi.config.get('info.strapi'),
|
||||
projectType: isEE ? 'Enterprise' : 'Community',
|
||||
useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
|
||||
useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
|
||||
projectId: uuid,
|
||||
isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud',
|
||||
};
|
||||
|
||||
addPackageJsonStrapiMetadata(anonymousMetadata, strapi);
|
||||
addPackageJsonStrapiMetadata(anonymousGroupProperties, strapi);
|
||||
|
||||
return async (event, payload = {}, opts = {}) => {
|
||||
const userId = generateAdminUserHash();
|
||||
|
||||
const reqParams = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event,
|
||||
uuid,
|
||||
userId,
|
||||
deviceId,
|
||||
properties: stringifyDeep({
|
||||
...payload,
|
||||
...anonymousMetadata,
|
||||
}),
|
||||
eventProperties: payload.eventProperties,
|
||||
userProperties: userId ? { ...anonymousUserProperties, ...payload.userProperties } : {},
|
||||
groupProperties: {
|
||||
...anonymousGroupProperties,
|
||||
...payload.groupProperties,
|
||||
},
|
||||
}),
|
||||
..._.merge({}, defaultQueryOpts, opts),
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${ANALYTICS_URI}/track`, reqParams);
|
||||
const res = await fetch(`${ANALYTICS_URI}/api/v2/track`, reqParams);
|
||||
return res.ok;
|
||||
} catch (err) {
|
||||
return false;
|
||||
|
@ -17,7 +17,7 @@ try {
|
||||
process.env.npm_config_global === 'true' ||
|
||||
JSON.parse(process.env.npm_config_argv).original.includes('global')
|
||||
) {
|
||||
fetch('https://analytics.strapi.io/track', {
|
||||
fetch('https://analytics.strapi.io/api/v2/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event: 'didInstallStrapi',
|
||||
|
@ -40,10 +40,12 @@ module.exports = {
|
||||
|
||||
if (deletedFiles.length + deletedFolders.length > 1) {
|
||||
strapi.telemetry.send('didBulkDeleteMediaLibraryElements', {
|
||||
rootFolderNumber: deletedFolders.length,
|
||||
rootAssetNumber: deletedFiles.length,
|
||||
totalFolderNumber,
|
||||
totalAssetNumber: totalFileNumber + deletedFiles.length,
|
||||
eventProperties: {
|
||||
rootFolderNumber: deletedFolders.length,
|
||||
rootAssetNumber: deletedFiles.length,
|
||||
totalFolderNumber,
|
||||
totalAssetNumber: totalFileNumber + deletedFiles.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -229,10 +231,12 @@ module.exports = {
|
||||
});
|
||||
|
||||
strapi.telemetry.send('didBulkMoveMediaLibraryElements', {
|
||||
rootFolderNumber: updatedFolders.length,
|
||||
rootAssetNumber: updatedFiles.length,
|
||||
totalFolderNumber,
|
||||
totalAssetNumber: totalFileNumber + updatedFiles.length,
|
||||
eventProperties: {
|
||||
rootFolderNumber: updatedFolders.length,
|
||||
rootAssetNumber: updatedFiles.length,
|
||||
totalFolderNumber,
|
||||
totalAssetNumber: totalFileNumber + updatedFiles.length,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
|
@ -91,7 +91,9 @@ module.exports = ({ strapi }) => ({
|
||||
|
||||
async sendMetrics() {
|
||||
const metrics = await this.computeMetrics();
|
||||
strapi.telemetry.send('didSendUploadPropertiesOnceAWeek', metrics);
|
||||
strapi.telemetry.send('didSendUploadPropertiesOnceAWeek', {
|
||||
groupProperties: { metrics },
|
||||
});
|
||||
|
||||
const metricsInfoStored = await getMetricsStoreValue();
|
||||
await setMetricsStoreValue({ ...metricsInfoStored, lastWeeklyUpdate: new Date().getTime() });
|
||||
|
@ -340,6 +340,7 @@ module.exports = ({ strapi }) => ({
|
||||
if (user) {
|
||||
fileValues[UPDATED_BY_ATTRIBUTE] = user.id;
|
||||
}
|
||||
|
||||
sendMediaMetrics(fileValues);
|
||||
|
||||
const res = await strapi.entityService.update(FILE_MODEL_UID, id, { data: fileValues });
|
||||
@ -355,6 +356,7 @@ module.exports = ({ strapi }) => ({
|
||||
fileValues[UPDATED_BY_ATTRIBUTE] = user.id;
|
||||
fileValues[CREATED_BY_ATTRIBUTE] = user.id;
|
||||
}
|
||||
|
||||
sendMediaMetrics(fileValues);
|
||||
|
||||
const res = await strapi.query(FILE_MODEL_UID).create({ data: fileValues });
|
||||
|
@ -53,33 +53,46 @@ function captureStderr(name, error) {
|
||||
return captureError(name);
|
||||
}
|
||||
|
||||
const getProperties = (scope, error) => ({
|
||||
error: typeof error === 'string' ? error : error && error.message,
|
||||
os: os.type(),
|
||||
osPlatform: os.platform(),
|
||||
osArch: os.arch(),
|
||||
osRelease: os.release(),
|
||||
version: scope.strapiVersion,
|
||||
nodeVersion: process.versions.node,
|
||||
docker: scope.docker,
|
||||
useYarn: scope.useYarn,
|
||||
useTypescriptOnServer: scope.useTypescript,
|
||||
useTypescriptOnAdmin: scope.useTypescript,
|
||||
isHostedOnStrapiCloud: process.env.STRAPI_HOSTING === 'strapi.cloud',
|
||||
noRun: (scope.runQuickstartApp !== true).toString(),
|
||||
});
|
||||
const getProperties = (scope, error) => {
|
||||
const eventProperties = {
|
||||
error: typeof error === 'string' ? error : error && error.message,
|
||||
};
|
||||
const userProperties = {
|
||||
os: os.type(),
|
||||
osPlatform: os.platform(),
|
||||
osArch: os.arch(),
|
||||
osRelease: os.release(),
|
||||
nodeVersion: process.versions.node,
|
||||
};
|
||||
const groupProperties = {
|
||||
version: scope.strapiVersion,
|
||||
docker: scope.docker,
|
||||
useYarn: scope.useYarn,
|
||||
useTypescriptOnServer: scope.useTypescript,
|
||||
useTypescriptOnAdmin: scope.useTypescript,
|
||||
isHostedOnStrapiCloud: process.env.STRAPI_HOSTING === 'strapi.cloud',
|
||||
noRun: (scope.runQuickstartApp !== true).toString(),
|
||||
projectId: scope.uuid,
|
||||
};
|
||||
|
||||
function trackEvent(event, body) {
|
||||
return {
|
||||
eventProperties,
|
||||
userProperties,
|
||||
groupProperties: addPackageJsonStrapiMetadata(groupProperties, scope),
|
||||
};
|
||||
};
|
||||
|
||||
function trackEvent(event, payload) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return fetch('https://analytics.strapi.io/track', {
|
||||
return fetch('https://analytics.strapi.io/api/v2/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event,
|
||||
...body,
|
||||
...payload,
|
||||
}),
|
||||
timeout: 1000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -91,14 +104,12 @@ function trackEvent(event, body) {
|
||||
}
|
||||
|
||||
function trackError({ scope, error }) {
|
||||
const { uuid } = scope;
|
||||
const properties = getProperties(scope, error);
|
||||
|
||||
try {
|
||||
return trackEvent('didNotCreateProject', {
|
||||
uuid,
|
||||
deviceId: scope.deviceId,
|
||||
properties: addPackageJsonStrapiMetadata(properties, scope),
|
||||
...properties,
|
||||
});
|
||||
} catch (err) {
|
||||
/** ignore errors */
|
||||
@ -107,14 +118,12 @@ function trackError({ scope, error }) {
|
||||
}
|
||||
|
||||
function trackUsage({ event, scope, error }) {
|
||||
const { uuid } = scope;
|
||||
const properties = getProperties(scope, error);
|
||||
|
||||
try {
|
||||
return trackEvent(event, {
|
||||
uuid,
|
||||
deviceId: scope.deviceId,
|
||||
properties: addPackageJsonStrapiMetadata(properties, scope),
|
||||
...properties,
|
||||
});
|
||||
} catch (err) {
|
||||
/** ignore errors */
|
||||
|
@ -44,7 +44,9 @@ describe('Metrics', () => {
|
||||
await sendDidInitializeEvent();
|
||||
|
||||
expect(strapi.telemetry.send).toHaveBeenCalledWith('didInitializeI18n', {
|
||||
numberOfContentTypes: 1,
|
||||
groupProperties: {
|
||||
numberOfContentTypes: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -88,7 +90,9 @@ describe('Metrics', () => {
|
||||
await sendDidUpdateI18nLocalesEvent();
|
||||
|
||||
expect(strapi.telemetry.send).toHaveBeenCalledWith('didUpdateI18nLocales', {
|
||||
numberOfLocales: 3,
|
||||
groupProperties: {
|
||||
numberOfLocales: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,13 +11,15 @@ const sendDidInitializeEvent = async () => {
|
||||
0
|
||||
)(strapi.contentTypes);
|
||||
|
||||
await strapi.telemetry.send('didInitializeI18n', { numberOfContentTypes });
|
||||
await strapi.telemetry.send('didInitializeI18n', { groupProperties: { numberOfContentTypes } });
|
||||
};
|
||||
|
||||
const sendDidUpdateI18nLocalesEvent = async () => {
|
||||
const numberOfLocales = await getService('locales').count();
|
||||
|
||||
await strapi.telemetry.send('didUpdateI18nLocales', { numberOfLocales });
|
||||
await strapi.telemetry.send('didUpdateI18nLocales', {
|
||||
groupProperties: { numberOfLocales },
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = () => ({
|
||||
|
Loading…
x
Reference in New Issue
Block a user