@astral/ui 4.83.0 → 4.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_mocks/LoggerMock/LoggerMock.d.ts +16 -0
- package/_mocks/LoggerMock/LoggerMock.js +18 -0
- package/_mocks/LoggerMock/index.d.ts +1 -0
- package/_mocks/LoggerMock/index.js +1 -0
- package/components/ConfigProvider/ConfigProvider.d.ts +7 -1
- package/components/ConfigProvider/ConfigProvider.js +14 -8
- package/components/DashboardLayout/Header/useLogic/useLogic.d.ts +1 -1
- package/components/Onboarding/Onboarding.js +13 -3
- package/components/Onboarding/core/ExitController/ExitController.d.ts +3 -1
- package/components/Onboarding/core/ExitController/ExitController.js +4 -1
- package/components/Onboarding/core/OnboardingTour/OnboardingTour.d.ts +3 -1
- package/components/Onboarding/core/OnboardingTour/OnboardingTour.js +25 -4
- package/components/Onboarding/core/ProgressStorage/ProgressStorage.d.ts +3 -1
- package/components/Onboarding/core/ProgressStorage/ProgressStorage.js +8 -4
- package/node/_mocks/LoggerMock/LoggerMock.d.ts +16 -0
- package/node/_mocks/LoggerMock/LoggerMock.js +23 -0
- package/node/_mocks/LoggerMock/index.d.ts +1 -0
- package/node/_mocks/LoggerMock/index.js +17 -0
- package/node/components/ConfigProvider/ConfigProvider.d.ts +7 -1
- package/node/components/ConfigProvider/ConfigProvider.js +13 -7
- package/node/components/DashboardLayout/Header/useLogic/useLogic.d.ts +1 -1
- package/node/components/Onboarding/Onboarding.js +13 -3
- package/node/components/Onboarding/core/ExitController/ExitController.d.ts +3 -1
- package/node/components/Onboarding/core/ExitController/ExitController.js +4 -1
- package/node/components/Onboarding/core/OnboardingTour/OnboardingTour.d.ts +3 -1
- package/node/components/Onboarding/core/OnboardingTour/OnboardingTour.js +25 -4
- package/node/components/Onboarding/core/ProgressStorage/ProgressStorage.d.ts +3 -1
- package/node/components/Onboarding/core/ProgressStorage/ProgressStorage.js +8 -4
- package/node/services/Logger/Logger.d.ts +45 -0
- package/node/services/Logger/Logger.js +72 -0
- package/node/services/Logger/index.d.ts +10 -0
- package/node/services/Logger/index.js +17 -0
- package/package.json +1 -1
- package/services/Logger/Logger.d.ts +45 -0
- package/services/Logger/Logger.js +68 -0
- package/services/Logger/index.d.ts +10 -0
- package/services/Logger/index.js +12 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Logger } from '../../services/Logger';
|
|
2
|
+
/**
|
|
3
|
+
* Mock логгера для тестов
|
|
4
|
+
*/
|
|
5
|
+
export declare class LoggerMock {
|
|
6
|
+
configure: Logger['configure'];
|
|
7
|
+
child: Logger['child'];
|
|
8
|
+
isLevelEnabled: Logger['isLevelEnabled'];
|
|
9
|
+
success: Logger['success'];
|
|
10
|
+
info: Logger['info'];
|
|
11
|
+
warn: Logger['warn'];
|
|
12
|
+
debug: Logger['debug'];
|
|
13
|
+
error: Logger['error'];
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare const createLoggerMock: () => Logger;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Mock логгера для тестов
|
|
4
|
+
*/
|
|
5
|
+
export class LoggerMock {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.configure = vi.fn(() => vi.fn());
|
|
8
|
+
this.child = vi.fn();
|
|
9
|
+
this.isLevelEnabled = vi.fn(() => false);
|
|
10
|
+
this.success = vi.fn();
|
|
11
|
+
this.info = vi.fn();
|
|
12
|
+
this.warn = vi.fn();
|
|
13
|
+
this.debug = vi.fn();
|
|
14
|
+
this.error = vi.fn();
|
|
15
|
+
vi.mocked(this.child).mockReturnValue(this);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const createLoggerMock = () => new LoggerMock();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoggerMock';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoggerMock';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type LogThreshold } from '../../services/Logger';
|
|
2
3
|
import { type LanguageMap } from '../DatePicker/types';
|
|
3
4
|
import { type PlaceholderImageProps } from '../placeholders/Placeholder/Image';
|
|
4
5
|
import { type CaptureException } from '../services/ErrorService';
|
|
@@ -138,7 +139,12 @@ export type ConfigProviderProps = Partial<ConfigContextProps> & {
|
|
|
138
139
|
* Переданный в компонент флаг напрямую имеет приоритет выше
|
|
139
140
|
*/
|
|
140
141
|
nextFeatureFlags?: NextFeatureFlagsContextProps;
|
|
142
|
+
/**
|
|
143
|
+
* Порог логов ui-kit в консоли. По умолчанию: prod → `error`, dev → `warn`,
|
|
144
|
+
* test → `silent`. `silent` — выключить полностью.
|
|
145
|
+
*/
|
|
146
|
+
logLevel?: LogThreshold;
|
|
141
147
|
};
|
|
142
148
|
export declare const ConfigContext: import("react").Context<ConfigContextProps>;
|
|
143
|
-
export declare const ConfigProvider: ({ children, language, datePickerLanguageMap, captureException: captureExceptionProp, emptySymbol, imagesMap, techSup, hidePersonalData, hidePersonalDataClassname, hidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags, }: Partial<ConfigProviderProps>) => import("react/jsx-runtime").JSX.Element;
|
|
149
|
+
export declare const ConfigProvider: ({ children, language, datePickerLanguageMap, captureException: captureExceptionProp, emptySymbol, imagesMap, techSup, hidePersonalData, hidePersonalDataClassname, hidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags, logLevel, }: Partial<ConfigProviderProps>) => import("react/jsx-runtime").JSX.Element;
|
|
144
150
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useEffect,
|
|
2
|
+
import { createContext, useEffect, } from 'react';
|
|
3
|
+
import { configureLogger } from '../../services/Logger';
|
|
3
4
|
import { russianMap } from '../DatePicker/constants';
|
|
4
5
|
import { ErrorService } from '../services/ErrorService';
|
|
5
6
|
import { NextFeatureFlagsContext, } from './NextFeatureFlagsContext';
|
|
@@ -13,6 +14,10 @@ const techSupDefault = {
|
|
|
13
14
|
phone: '',
|
|
14
15
|
email: '',
|
|
15
16
|
};
|
|
17
|
+
const defaultCaptureException = (error) => {
|
|
18
|
+
// biome-ignore lint/suspicious/noConsole: Ошибка для интеграторов
|
|
19
|
+
console.error(error);
|
|
20
|
+
};
|
|
16
21
|
const defaultHidePersonalDataClassname = 'ym-hide-content sentry-mask';
|
|
17
22
|
const defaultHidePersonalInputDataClassname = 'ym-disable-keys sentry-mask';
|
|
18
23
|
export const ConfigContext = createContext({
|
|
@@ -27,20 +32,21 @@ export const ConfigContext = createContext({
|
|
|
27
32
|
hidePersonalInputDataClassname: defaultHidePersonalInputDataClassname,
|
|
28
33
|
hidePersonalData: true,
|
|
29
34
|
});
|
|
30
|
-
export const ConfigProvider = ({ children, language = 'ru', datePickerLanguageMap = russianMap, captureException: captureExceptionProp, emptySymbol = '—', imagesMap = imagesMapDefault, techSup = techSupDefault, hidePersonalData = true, hidePersonalDataClassname = defaultHidePersonalDataClassname, hidePersonalInputDataClassname = defaultHidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags = {}, }) => {
|
|
31
|
-
const captureException =
|
|
32
|
-
|
|
33
|
-
captureExceptionProp || ((error) => console.error(error));
|
|
34
|
-
useMemo(() => {
|
|
35
|
+
export const ConfigProvider = ({ children, language = 'ru', datePickerLanguageMap = russianMap, captureException: captureExceptionProp, emptySymbol = '—', imagesMap = imagesMapDefault, techSup = techSupDefault, hidePersonalData = true, hidePersonalDataClassname = defaultHidePersonalDataClassname, hidePersonalInputDataClassname = defaultHidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags = {}, logLevel, }) => {
|
|
36
|
+
const captureException = captureExceptionProp ?? defaultCaptureException;
|
|
37
|
+
useEffect(() => {
|
|
35
38
|
ErrorService.getInstance().init(captureException);
|
|
36
39
|
}, [captureException]);
|
|
37
40
|
useEffect(() => {
|
|
38
|
-
|
|
41
|
+
return configureLogger({ level: logLevel });
|
|
42
|
+
}, [logLevel]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!captureExceptionProp) {
|
|
39
45
|
// biome-ignore lint/suspicious/noConsole: Ошибка для интеграторов
|
|
40
46
|
console.warn('ConfigProvider: Необходимо наличие captureException, связанного с сервисом мониторинга ошибок.\n' +
|
|
41
47
|
'На данный момент все ошибки, отлавливаемые в ErrorBoundary будут выводиться только в консоль');
|
|
42
48
|
}
|
|
43
|
-
}, []);
|
|
49
|
+
}, [captureExceptionProp]);
|
|
44
50
|
const components = {
|
|
45
51
|
...propsComponents,
|
|
46
52
|
layout: {
|
|
@@ -5,7 +5,7 @@ export declare const useLogic: ({ profile, menuOrganization }: HeaderProps) => {
|
|
|
5
5
|
isShowProfile: boolean;
|
|
6
6
|
isMobile: boolean;
|
|
7
7
|
isFocusedMode: boolean;
|
|
8
|
-
mobileHeaderPriorityFeature: "
|
|
8
|
+
mobileHeaderPriorityFeature: "profile" | "menuOrg";
|
|
9
9
|
isLoading: boolean;
|
|
10
10
|
collapsedIn: boolean;
|
|
11
11
|
onToggleSidebar: (newValue?: boolean | undefined) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from '../../services/Logger';
|
|
1
2
|
import { ErrorService } from '../services/ErrorService';
|
|
2
3
|
import { ActiveStepStore } from './core/ActiveStepStore';
|
|
3
4
|
import { OnboardingTour } from './core/OnboardingTour';
|
|
@@ -18,11 +19,20 @@ export class Onboarding {
|
|
|
18
19
|
* Создаёт тур.
|
|
19
20
|
*/
|
|
20
21
|
this.createTour = (tourConfig) => {
|
|
22
|
+
const logger = createLogger('Onboarding', {
|
|
23
|
+
context: { tourName: tourConfig.name },
|
|
24
|
+
});
|
|
21
25
|
// Fail-fast на некорректной конфигурации до сборки тура.
|
|
22
|
-
|
|
26
|
+
try {
|
|
27
|
+
validateTourConfig(tourConfig);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.error('Некорректная конфигурация тура', error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
23
33
|
// Ключ хранилища уникален на тур, поэтому стратегия строится на createTour.
|
|
24
|
-
const progressStorage = new ProgressStorage(this.saveStrategyFactory(tourConfig.name));
|
|
25
|
-
return new OnboardingTour(tourConfig, progressStorage, this.activeStepStore, this.errorService);
|
|
34
|
+
const progressStorage = new ProgressStorage(this.saveStrategyFactory(tourConfig.name), logger);
|
|
35
|
+
return new OnboardingTour(tourConfig, progressStorage, this.activeStepStore, this.errorService, logger);
|
|
26
36
|
};
|
|
27
37
|
/**
|
|
28
38
|
* Закрывает показанный сейчас тур.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
export type ExitControllerParams = {
|
|
2
3
|
/**
|
|
3
4
|
* Фактическое закрытие тура. Вызывается только после подтверждения в
|
|
@@ -20,8 +21,9 @@ export type ExitControllerParams = {
|
|
|
20
21
|
*/
|
|
21
22
|
export declare class ExitController {
|
|
22
23
|
private readonly params;
|
|
24
|
+
private readonly logger;
|
|
23
25
|
private confirming;
|
|
24
|
-
constructor(params: ExitControllerParams);
|
|
26
|
+
constructor(params: ExitControllerParams, logger: Logger);
|
|
25
27
|
/**
|
|
26
28
|
* Идёт ли запрос подтверждения выхода.
|
|
27
29
|
*/
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* (ConfirmExit); закрытие происходит только по confirm.
|
|
7
7
|
*/
|
|
8
8
|
export class ExitController {
|
|
9
|
-
constructor(params) {
|
|
9
|
+
constructor(params, logger) {
|
|
10
10
|
this.params = params;
|
|
11
|
+
this.logger = logger;
|
|
11
12
|
this.confirming = false;
|
|
12
13
|
/**
|
|
13
14
|
* Запрос на закрытие — показывает ConfirmExit. Повторные вызовы во время
|
|
@@ -17,6 +18,7 @@ export class ExitController {
|
|
|
17
18
|
if (this.confirming) {
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
21
|
+
this.logger.debug('Запрошен выход из тура, показано подтверждение');
|
|
20
22
|
this.confirming = true;
|
|
21
23
|
this.params.onChange();
|
|
22
24
|
};
|
|
@@ -37,6 +39,7 @@ export class ExitController {
|
|
|
37
39
|
if (!this.confirming) {
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
42
|
+
this.logger.debug('Выход из тура отменён');
|
|
40
43
|
this.confirming = false;
|
|
41
44
|
this.params.onChange();
|
|
42
45
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
import { type ErrorService } from '../../../services/ErrorService';
|
|
2
3
|
import { type OnboardingTourConfig, type Step, type StepHandles, type TourProgress } from '../../types';
|
|
3
4
|
import { type ActiveStepStore } from '../ActiveStepStore';
|
|
@@ -13,6 +14,7 @@ export declare class OnboardingTour<TSteps extends readonly Step[] = readonly St
|
|
|
13
14
|
private readonly progressStorage;
|
|
14
15
|
private readonly activeStepStore;
|
|
15
16
|
private readonly errorService;
|
|
17
|
+
private readonly logger;
|
|
16
18
|
private readonly navigator;
|
|
17
19
|
private readonly resolver;
|
|
18
20
|
private readonly stepsController;
|
|
@@ -22,7 +24,7 @@ export declare class OnboardingTour<TSteps extends readonly Step[] = readonly St
|
|
|
22
24
|
* Типобезопасный доступ к шагам по их именам.
|
|
23
25
|
*/
|
|
24
26
|
readonly steps: StepHandles<TSteps>;
|
|
25
|
-
constructor(config: OnboardingTourConfig<TSteps>, progressStorage: ProgressStorage, activeStepStore: ActiveStepStore, errorService: ErrorService);
|
|
27
|
+
constructor(config: OnboardingTourConfig<TSteps>, progressStorage: ProgressStorage, activeStepStore: ActiveStepStore, errorService: ErrorService, logger: Logger);
|
|
26
28
|
/**
|
|
27
29
|
* Запуск тура с первого шага.
|
|
28
30
|
*/
|
|
@@ -9,11 +9,12 @@ import { StepsController } from '../StepsController';
|
|
|
9
9
|
* состояние активного шага в store своего экземпляра онбординга.
|
|
10
10
|
*/
|
|
11
11
|
export class OnboardingTour {
|
|
12
|
-
constructor(config, progressStorage, activeStepStore, errorService) {
|
|
12
|
+
constructor(config, progressStorage, activeStepStore, errorService, logger) {
|
|
13
13
|
this.config = config;
|
|
14
14
|
this.progressStorage = progressStorage;
|
|
15
15
|
this.activeStepStore = activeStepStore;
|
|
16
16
|
this.errorService = errorService;
|
|
17
|
+
this.logger = logger;
|
|
17
18
|
this.status = 'idle';
|
|
18
19
|
/**
|
|
19
20
|
* Запуск тура с первого шага.
|
|
@@ -21,8 +22,10 @@ export class OnboardingTour {
|
|
|
21
22
|
this.start = async () => {
|
|
22
23
|
const persisted = await this.progressStorage.load();
|
|
23
24
|
if (persisted?.isDismissed) {
|
|
25
|
+
this.logger.info('Тур пропущен: ранее закрыт пользователем');
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
28
|
+
this.logger.info('Тур запущен');
|
|
26
29
|
this.exitController.reset();
|
|
27
30
|
this.navigator.reset();
|
|
28
31
|
this.enterCurrentStep();
|
|
@@ -32,8 +35,10 @@ export class OnboardingTour {
|
|
|
32
35
|
*/
|
|
33
36
|
this.resume = () => {
|
|
34
37
|
if (this.status !== 'waiting-resume') {
|
|
38
|
+
this.logger.debug('resume проигнорирован: тур не на паузе');
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
41
|
+
this.logger.debug('Тур возобновлён после паузы');
|
|
37
42
|
this.status = 'running';
|
|
38
43
|
this.publishStep();
|
|
39
44
|
};
|
|
@@ -42,6 +47,7 @@ export class OnboardingTour {
|
|
|
42
47
|
*/
|
|
43
48
|
this.restore = async () => {
|
|
44
49
|
const persisted = await this.progressStorage.load();
|
|
50
|
+
this.logger.info('Тур восстановлен');
|
|
45
51
|
this.exitController.reset();
|
|
46
52
|
this.navigator.reset();
|
|
47
53
|
// Пройденный тур переигрываем с начала; иначе продолжаем с сохранённого шага.
|
|
@@ -148,6 +154,7 @@ export class OnboardingTour {
|
|
|
148
154
|
*/
|
|
149
155
|
this.resetProgress = async () => {
|
|
150
156
|
await this.progressStorage.reset();
|
|
157
|
+
this.logger.debug('Прогресс тура сброшен');
|
|
151
158
|
};
|
|
152
159
|
/**
|
|
153
160
|
* Возвращает сохранённый прогресс тура
|
|
@@ -167,12 +174,14 @@ export class OnboardingTour {
|
|
|
167
174
|
isDismissed: true,
|
|
168
175
|
});
|
|
169
176
|
}
|
|
177
|
+
this.logger.info('Тур закрыт', { reason });
|
|
170
178
|
this.status = 'idle';
|
|
171
179
|
this.exitController.reset();
|
|
172
180
|
this.releaseStep();
|
|
173
181
|
this.config.onClose?.(reason);
|
|
174
182
|
};
|
|
175
183
|
this.handleError = (error) => {
|
|
184
|
+
this.logger.error('Ошибка тура', error);
|
|
176
185
|
this.errorService.captureException(error, {
|
|
177
186
|
place: 'Onboarding',
|
|
178
187
|
tourName: this.config.name,
|
|
@@ -185,17 +194,22 @@ export class OnboardingTour {
|
|
|
185
194
|
this.resolver = new StepResolver(config);
|
|
186
195
|
this.stepsController = new StepsController(config.steps, (stepName) => this.refreshStep(stepName));
|
|
187
196
|
this.steps = this.stepsController.handles;
|
|
188
|
-
|
|
197
|
+
const exitParams = {
|
|
189
198
|
close: () => this.exit('dismissed'),
|
|
190
199
|
onChange: () => this.publishStep(),
|
|
191
|
-
}
|
|
200
|
+
};
|
|
201
|
+
this.exitController = new ExitController(exitParams, this.logger);
|
|
192
202
|
}
|
|
193
203
|
/**
|
|
194
204
|
* Разрешена ли навигация сейчас: тур показан на экране и владеет активным
|
|
195
205
|
* шагом store
|
|
196
206
|
*/
|
|
197
207
|
canNavigate() {
|
|
198
|
-
|
|
208
|
+
const isNavigable = this.status === 'running' && this.activeStepStore.isActiveTour(this);
|
|
209
|
+
if (!isNavigable) {
|
|
210
|
+
this.logger.debug('Навигация проигнорирована: тур неактивен');
|
|
211
|
+
}
|
|
212
|
+
return isNavigable;
|
|
199
213
|
}
|
|
200
214
|
/**
|
|
201
215
|
* Переход к текущему шагу. Шаг с `waitForResume` ставит тур в ожидание
|
|
@@ -204,16 +218,23 @@ export class OnboardingTour {
|
|
|
204
218
|
enterCurrentStep() {
|
|
205
219
|
// Показывать нечего: у тура нет ни одного видимого (не-пропущенного) шага.
|
|
206
220
|
if (this.navigator.total === 0) {
|
|
221
|
+
this.logger.warn('Нет ни одного видимого шага, тур не показан');
|
|
207
222
|
this.status = 'idle';
|
|
208
223
|
this.releaseStep();
|
|
209
224
|
return;
|
|
210
225
|
}
|
|
211
226
|
const step = this.navigator.current;
|
|
212
227
|
if (step.waitForResume) {
|
|
228
|
+
this.logger.debug('Ожидание resume', { step: step.name });
|
|
213
229
|
this.status = 'waiting-resume';
|
|
214
230
|
this.releaseStep();
|
|
215
231
|
return;
|
|
216
232
|
}
|
|
233
|
+
this.logger.debug('Показан шаг', {
|
|
234
|
+
step: step.name,
|
|
235
|
+
position: this.navigator.position,
|
|
236
|
+
total: this.navigator.total,
|
|
237
|
+
});
|
|
217
238
|
this.status = 'running';
|
|
218
239
|
this.publishStep();
|
|
219
240
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
import { type SaveStrategy, type TourProgress } from '../../types';
|
|
2
3
|
/**
|
|
3
4
|
* Обёртка над переданной SaveStrategy, изолирующая тур от исключений хранилища
|
|
@@ -9,8 +10,9 @@ import { type SaveStrategy, type TourProgress } from '../../types';
|
|
|
9
10
|
*/
|
|
10
11
|
export declare class ProgressStorage {
|
|
11
12
|
private readonly strategy;
|
|
13
|
+
private readonly logger;
|
|
12
14
|
private saving;
|
|
13
|
-
constructor(strategy: SaveStrategy);
|
|
15
|
+
constructor(strategy: SaveStrategy, logger: Logger);
|
|
14
16
|
load(): Promise<TourProgress | null>;
|
|
15
17
|
save(state: TourProgress): Promise<void>;
|
|
16
18
|
/**
|
|
@@ -15,8 +15,9 @@ const CLEARED_STATE = {
|
|
|
15
15
|
* `load()` ждёт текущее сохранение, поэтому читает уже применённое состояние.
|
|
16
16
|
*/
|
|
17
17
|
export class ProgressStorage {
|
|
18
|
-
constructor(strategy) {
|
|
18
|
+
constructor(strategy, logger) {
|
|
19
19
|
this.strategy = strategy;
|
|
20
|
+
this.logger = logger;
|
|
20
21
|
this.saving = Promise.resolve();
|
|
21
22
|
}
|
|
22
23
|
async load() {
|
|
@@ -25,7 +26,9 @@ export class ProgressStorage {
|
|
|
25
26
|
try {
|
|
26
27
|
return await this.strategy.load();
|
|
27
28
|
}
|
|
28
|
-
catch {
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Ошибка хранилища не должна ронять тур, но остаётся видимой в логах.
|
|
31
|
+
this.logger.warn('Ошибка чтения прогресса из хранилища', error);
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -49,8 +52,9 @@ export class ProgressStorage {
|
|
|
49
52
|
try {
|
|
50
53
|
await write();
|
|
51
54
|
}
|
|
52
|
-
catch {
|
|
53
|
-
// Ошибка хранилища не должна ронять
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Ошибка хранилища не должна ронять тур, но остаётся видимой в логах.
|
|
57
|
+
this.logger.warn('Ошибка записи прогресса в хранилище', error);
|
|
54
58
|
}
|
|
55
59
|
});
|
|
56
60
|
return this.saving;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Logger } from '../../services/Logger';
|
|
2
|
+
/**
|
|
3
|
+
* Mock логгера для тестов
|
|
4
|
+
*/
|
|
5
|
+
export declare class LoggerMock {
|
|
6
|
+
configure: Logger['configure'];
|
|
7
|
+
child: Logger['child'];
|
|
8
|
+
isLevelEnabled: Logger['isLevelEnabled'];
|
|
9
|
+
success: Logger['success'];
|
|
10
|
+
info: Logger['info'];
|
|
11
|
+
warn: Logger['warn'];
|
|
12
|
+
debug: Logger['debug'];
|
|
13
|
+
error: Logger['error'];
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare const createLoggerMock: () => Logger;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLoggerMock = exports.LoggerMock = void 0;
|
|
4
|
+
const vitest_1 = require("vitest");
|
|
5
|
+
/**
|
|
6
|
+
* Mock логгера для тестов
|
|
7
|
+
*/
|
|
8
|
+
class LoggerMock {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.configure = vitest_1.vi.fn(() => vitest_1.vi.fn());
|
|
11
|
+
this.child = vitest_1.vi.fn();
|
|
12
|
+
this.isLevelEnabled = vitest_1.vi.fn(() => false);
|
|
13
|
+
this.success = vitest_1.vi.fn();
|
|
14
|
+
this.info = vitest_1.vi.fn();
|
|
15
|
+
this.warn = vitest_1.vi.fn();
|
|
16
|
+
this.debug = vitest_1.vi.fn();
|
|
17
|
+
this.error = vitest_1.vi.fn();
|
|
18
|
+
vitest_1.vi.mocked(this.child).mockReturnValue(this);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.LoggerMock = LoggerMock;
|
|
22
|
+
const createLoggerMock = () => new LoggerMock();
|
|
23
|
+
exports.createLoggerMock = createLoggerMock;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoggerMock';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./LoggerMock"), exports);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type LogThreshold } from '../../services/Logger';
|
|
2
3
|
import { type LanguageMap } from '../DatePicker/types';
|
|
3
4
|
import { type PlaceholderImageProps } from '../placeholders/Placeholder/Image';
|
|
4
5
|
import { type CaptureException } from '../services/ErrorService';
|
|
@@ -138,7 +139,12 @@ export type ConfigProviderProps = Partial<ConfigContextProps> & {
|
|
|
138
139
|
* Переданный в компонент флаг напрямую имеет приоритет выше
|
|
139
140
|
*/
|
|
140
141
|
nextFeatureFlags?: NextFeatureFlagsContextProps;
|
|
142
|
+
/**
|
|
143
|
+
* Порог логов ui-kit в консоли. По умолчанию: prod → `error`, dev → `warn`,
|
|
144
|
+
* test → `silent`. `silent` — выключить полностью.
|
|
145
|
+
*/
|
|
146
|
+
logLevel?: LogThreshold;
|
|
141
147
|
};
|
|
142
148
|
export declare const ConfigContext: import("react").Context<ConfigContextProps>;
|
|
143
|
-
export declare const ConfigProvider: ({ children, language, datePickerLanguageMap, captureException: captureExceptionProp, emptySymbol, imagesMap, techSup, hidePersonalData, hidePersonalDataClassname, hidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags, }: Partial<ConfigProviderProps>) => import("react/jsx-runtime").JSX.Element;
|
|
149
|
+
export declare const ConfigProvider: ({ children, language, datePickerLanguageMap, captureException: captureExceptionProp, emptySymbol, imagesMap, techSup, hidePersonalData, hidePersonalDataClassname, hidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags, logLevel, }: Partial<ConfigProviderProps>) => import("react/jsx-runtime").JSX.Element;
|
|
144
150
|
export {};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ConfigProvider = exports.ConfigContext = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
+
const Logger_1 = require("../../services/Logger");
|
|
6
7
|
const constants_1 = require("../DatePicker/constants");
|
|
7
8
|
const ErrorService_1 = require("../services/ErrorService");
|
|
8
9
|
const NextFeatureFlagsContext_1 = require("./NextFeatureFlagsContext");
|
|
@@ -16,6 +17,10 @@ const techSupDefault = {
|
|
|
16
17
|
phone: '',
|
|
17
18
|
email: '',
|
|
18
19
|
};
|
|
20
|
+
const defaultCaptureException = (error) => {
|
|
21
|
+
// biome-ignore lint/suspicious/noConsole: Ошибка для интеграторов
|
|
22
|
+
console.error(error);
|
|
23
|
+
};
|
|
19
24
|
const defaultHidePersonalDataClassname = 'ym-hide-content sentry-mask';
|
|
20
25
|
const defaultHidePersonalInputDataClassname = 'ym-disable-keys sentry-mask';
|
|
21
26
|
exports.ConfigContext = (0, react_1.createContext)({
|
|
@@ -30,20 +35,21 @@ exports.ConfigContext = (0, react_1.createContext)({
|
|
|
30
35
|
hidePersonalInputDataClassname: defaultHidePersonalInputDataClassname,
|
|
31
36
|
hidePersonalData: true,
|
|
32
37
|
});
|
|
33
|
-
const ConfigProvider = ({ children, language = 'ru', datePickerLanguageMap = constants_1.russianMap, captureException: captureExceptionProp, emptySymbol = '—', imagesMap = imagesMapDefault, techSup = techSupDefault, hidePersonalData = true, hidePersonalDataClassname = defaultHidePersonalDataClassname, hidePersonalInputDataClassname = defaultHidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags = {}, }) => {
|
|
34
|
-
const captureException =
|
|
35
|
-
|
|
36
|
-
captureExceptionProp || ((error) => console.error(error));
|
|
37
|
-
(0, react_1.useMemo)(() => {
|
|
38
|
+
const ConfigProvider = ({ children, language = 'ru', datePickerLanguageMap = constants_1.russianMap, captureException: captureExceptionProp, emptySymbol = '—', imagesMap = imagesMapDefault, techSup = techSupDefault, hidePersonalData = true, hidePersonalDataClassname = defaultHidePersonalDataClassname, hidePersonalInputDataClassname = defaultHidePersonalInputDataClassname, components: propsComponents, nextFeatureFlags = {}, logLevel, }) => {
|
|
39
|
+
const captureException = captureExceptionProp ?? defaultCaptureException;
|
|
40
|
+
(0, react_1.useEffect)(() => {
|
|
38
41
|
ErrorService_1.ErrorService.getInstance().init(captureException);
|
|
39
42
|
}, [captureException]);
|
|
40
43
|
(0, react_1.useEffect)(() => {
|
|
41
|
-
|
|
44
|
+
return (0, Logger_1.configureLogger)({ level: logLevel });
|
|
45
|
+
}, [logLevel]);
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
if (!captureExceptionProp) {
|
|
42
48
|
// biome-ignore lint/suspicious/noConsole: Ошибка для интеграторов
|
|
43
49
|
console.warn('ConfigProvider: Необходимо наличие captureException, связанного с сервисом мониторинга ошибок.\n' +
|
|
44
50
|
'На данный момент все ошибки, отлавливаемые в ErrorBoundary будут выводиться только в консоль');
|
|
45
51
|
}
|
|
46
|
-
}, []);
|
|
52
|
+
}, [captureExceptionProp]);
|
|
47
53
|
const components = {
|
|
48
54
|
...propsComponents,
|
|
49
55
|
layout: {
|
|
@@ -5,7 +5,7 @@ export declare const useLogic: ({ profile, menuOrganization }: HeaderProps) => {
|
|
|
5
5
|
isShowProfile: boolean;
|
|
6
6
|
isMobile: boolean;
|
|
7
7
|
isFocusedMode: boolean;
|
|
8
|
-
mobileHeaderPriorityFeature: "
|
|
8
|
+
mobileHeaderPriorityFeature: "profile" | "menuOrg";
|
|
9
9
|
isLoading: boolean;
|
|
10
10
|
collapsedIn: boolean;
|
|
11
11
|
onToggleSidebar: (newValue?: boolean | undefined) => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createOnboarding = exports.Onboarding = void 0;
|
|
4
|
+
const Logger_1 = require("../../services/Logger");
|
|
4
5
|
const ErrorService_1 = require("../services/ErrorService");
|
|
5
6
|
const ActiveStepStore_1 = require("./core/ActiveStepStore");
|
|
6
7
|
const OnboardingTour_1 = require("./core/OnboardingTour");
|
|
@@ -21,11 +22,20 @@ class Onboarding {
|
|
|
21
22
|
* Создаёт тур.
|
|
22
23
|
*/
|
|
23
24
|
this.createTour = (tourConfig) => {
|
|
25
|
+
const logger = (0, Logger_1.createLogger)('Onboarding', {
|
|
26
|
+
context: { tourName: tourConfig.name },
|
|
27
|
+
});
|
|
24
28
|
// Fail-fast на некорректной конфигурации до сборки тура.
|
|
25
|
-
|
|
29
|
+
try {
|
|
30
|
+
(0, validation_1.validateTourConfig)(tourConfig);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger.error('Некорректная конфигурация тура', error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
26
36
|
// Ключ хранилища уникален на тур, поэтому стратегия строится на createTour.
|
|
27
|
-
const progressStorage = new ProgressStorage_1.ProgressStorage(this.saveStrategyFactory(tourConfig.name));
|
|
28
|
-
return new OnboardingTour_1.OnboardingTour(tourConfig, progressStorage, this.activeStepStore, this.errorService);
|
|
37
|
+
const progressStorage = new ProgressStorage_1.ProgressStorage(this.saveStrategyFactory(tourConfig.name), logger);
|
|
38
|
+
return new OnboardingTour_1.OnboardingTour(tourConfig, progressStorage, this.activeStepStore, this.errorService, logger);
|
|
29
39
|
};
|
|
30
40
|
/**
|
|
31
41
|
* Закрывает показанный сейчас тур.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
export type ExitControllerParams = {
|
|
2
3
|
/**
|
|
3
4
|
* Фактическое закрытие тура. Вызывается только после подтверждения в
|
|
@@ -20,8 +21,9 @@ export type ExitControllerParams = {
|
|
|
20
21
|
*/
|
|
21
22
|
export declare class ExitController {
|
|
22
23
|
private readonly params;
|
|
24
|
+
private readonly logger;
|
|
23
25
|
private confirming;
|
|
24
|
-
constructor(params: ExitControllerParams);
|
|
26
|
+
constructor(params: ExitControllerParams, logger: Logger);
|
|
25
27
|
/**
|
|
26
28
|
* Идёт ли запрос подтверждения выхода.
|
|
27
29
|
*/
|
|
@@ -9,8 +9,9 @@ exports.ExitController = void 0;
|
|
|
9
9
|
* (ConfirmExit); закрытие происходит только по confirm.
|
|
10
10
|
*/
|
|
11
11
|
class ExitController {
|
|
12
|
-
constructor(params) {
|
|
12
|
+
constructor(params, logger) {
|
|
13
13
|
this.params = params;
|
|
14
|
+
this.logger = logger;
|
|
14
15
|
this.confirming = false;
|
|
15
16
|
/**
|
|
16
17
|
* Запрос на закрытие — показывает ConfirmExit. Повторные вызовы во время
|
|
@@ -20,6 +21,7 @@ class ExitController {
|
|
|
20
21
|
if (this.confirming) {
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
24
|
+
this.logger.debug('Запрошен выход из тура, показано подтверждение');
|
|
23
25
|
this.confirming = true;
|
|
24
26
|
this.params.onChange();
|
|
25
27
|
};
|
|
@@ -40,6 +42,7 @@ class ExitController {
|
|
|
40
42
|
if (!this.confirming) {
|
|
41
43
|
return;
|
|
42
44
|
}
|
|
45
|
+
this.logger.debug('Выход из тура отменён');
|
|
43
46
|
this.confirming = false;
|
|
44
47
|
this.params.onChange();
|
|
45
48
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
import { type ErrorService } from '../../../services/ErrorService';
|
|
2
3
|
import { type OnboardingTourConfig, type Step, type StepHandles, type TourProgress } from '../../types';
|
|
3
4
|
import { type ActiveStepStore } from '../ActiveStepStore';
|
|
@@ -13,6 +14,7 @@ export declare class OnboardingTour<TSteps extends readonly Step[] = readonly St
|
|
|
13
14
|
private readonly progressStorage;
|
|
14
15
|
private readonly activeStepStore;
|
|
15
16
|
private readonly errorService;
|
|
17
|
+
private readonly logger;
|
|
16
18
|
private readonly navigator;
|
|
17
19
|
private readonly resolver;
|
|
18
20
|
private readonly stepsController;
|
|
@@ -22,7 +24,7 @@ export declare class OnboardingTour<TSteps extends readonly Step[] = readonly St
|
|
|
22
24
|
* Типобезопасный доступ к шагам по их именам.
|
|
23
25
|
*/
|
|
24
26
|
readonly steps: StepHandles<TSteps>;
|
|
25
|
-
constructor(config: OnboardingTourConfig<TSteps>, progressStorage: ProgressStorage, activeStepStore: ActiveStepStore, errorService: ErrorService);
|
|
27
|
+
constructor(config: OnboardingTourConfig<TSteps>, progressStorage: ProgressStorage, activeStepStore: ActiveStepStore, errorService: ErrorService, logger: Logger);
|
|
26
28
|
/**
|
|
27
29
|
* Запуск тура с первого шага.
|
|
28
30
|
*/
|
|
@@ -12,11 +12,12 @@ const StepsController_1 = require("../StepsController");
|
|
|
12
12
|
* состояние активного шага в store своего экземпляра онбординга.
|
|
13
13
|
*/
|
|
14
14
|
class OnboardingTour {
|
|
15
|
-
constructor(config, progressStorage, activeStepStore, errorService) {
|
|
15
|
+
constructor(config, progressStorage, activeStepStore, errorService, logger) {
|
|
16
16
|
this.config = config;
|
|
17
17
|
this.progressStorage = progressStorage;
|
|
18
18
|
this.activeStepStore = activeStepStore;
|
|
19
19
|
this.errorService = errorService;
|
|
20
|
+
this.logger = logger;
|
|
20
21
|
this.status = 'idle';
|
|
21
22
|
/**
|
|
22
23
|
* Запуск тура с первого шага.
|
|
@@ -24,8 +25,10 @@ class OnboardingTour {
|
|
|
24
25
|
this.start = async () => {
|
|
25
26
|
const persisted = await this.progressStorage.load();
|
|
26
27
|
if (persisted?.isDismissed) {
|
|
28
|
+
this.logger.info('Тур пропущен: ранее закрыт пользователем');
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
31
|
+
this.logger.info('Тур запущен');
|
|
29
32
|
this.exitController.reset();
|
|
30
33
|
this.navigator.reset();
|
|
31
34
|
this.enterCurrentStep();
|
|
@@ -35,8 +38,10 @@ class OnboardingTour {
|
|
|
35
38
|
*/
|
|
36
39
|
this.resume = () => {
|
|
37
40
|
if (this.status !== 'waiting-resume') {
|
|
41
|
+
this.logger.debug('resume проигнорирован: тур не на паузе');
|
|
38
42
|
return;
|
|
39
43
|
}
|
|
44
|
+
this.logger.debug('Тур возобновлён после паузы');
|
|
40
45
|
this.status = 'running';
|
|
41
46
|
this.publishStep();
|
|
42
47
|
};
|
|
@@ -45,6 +50,7 @@ class OnboardingTour {
|
|
|
45
50
|
*/
|
|
46
51
|
this.restore = async () => {
|
|
47
52
|
const persisted = await this.progressStorage.load();
|
|
53
|
+
this.logger.info('Тур восстановлен');
|
|
48
54
|
this.exitController.reset();
|
|
49
55
|
this.navigator.reset();
|
|
50
56
|
// Пройденный тур переигрываем с начала; иначе продолжаем с сохранённого шага.
|
|
@@ -151,6 +157,7 @@ class OnboardingTour {
|
|
|
151
157
|
*/
|
|
152
158
|
this.resetProgress = async () => {
|
|
153
159
|
await this.progressStorage.reset();
|
|
160
|
+
this.logger.debug('Прогресс тура сброшен');
|
|
154
161
|
};
|
|
155
162
|
/**
|
|
156
163
|
* Возвращает сохранённый прогресс тура
|
|
@@ -170,12 +177,14 @@ class OnboardingTour {
|
|
|
170
177
|
isDismissed: true,
|
|
171
178
|
});
|
|
172
179
|
}
|
|
180
|
+
this.logger.info('Тур закрыт', { reason });
|
|
173
181
|
this.status = 'idle';
|
|
174
182
|
this.exitController.reset();
|
|
175
183
|
this.releaseStep();
|
|
176
184
|
this.config.onClose?.(reason);
|
|
177
185
|
};
|
|
178
186
|
this.handleError = (error) => {
|
|
187
|
+
this.logger.error('Ошибка тура', error);
|
|
179
188
|
this.errorService.captureException(error, {
|
|
180
189
|
place: 'Onboarding',
|
|
181
190
|
tourName: this.config.name,
|
|
@@ -188,17 +197,22 @@ class OnboardingTour {
|
|
|
188
197
|
this.resolver = new StepResolver_1.StepResolver(config);
|
|
189
198
|
this.stepsController = new StepsController_1.StepsController(config.steps, (stepName) => this.refreshStep(stepName));
|
|
190
199
|
this.steps = this.stepsController.handles;
|
|
191
|
-
|
|
200
|
+
const exitParams = {
|
|
192
201
|
close: () => this.exit('dismissed'),
|
|
193
202
|
onChange: () => this.publishStep(),
|
|
194
|
-
}
|
|
203
|
+
};
|
|
204
|
+
this.exitController = new ExitController_1.ExitController(exitParams, this.logger);
|
|
195
205
|
}
|
|
196
206
|
/**
|
|
197
207
|
* Разрешена ли навигация сейчас: тур показан на экране и владеет активным
|
|
198
208
|
* шагом store
|
|
199
209
|
*/
|
|
200
210
|
canNavigate() {
|
|
201
|
-
|
|
211
|
+
const isNavigable = this.status === 'running' && this.activeStepStore.isActiveTour(this);
|
|
212
|
+
if (!isNavigable) {
|
|
213
|
+
this.logger.debug('Навигация проигнорирована: тур неактивен');
|
|
214
|
+
}
|
|
215
|
+
return isNavigable;
|
|
202
216
|
}
|
|
203
217
|
/**
|
|
204
218
|
* Переход к текущему шагу. Шаг с `waitForResume` ставит тур в ожидание
|
|
@@ -207,16 +221,23 @@ class OnboardingTour {
|
|
|
207
221
|
enterCurrentStep() {
|
|
208
222
|
// Показывать нечего: у тура нет ни одного видимого (не-пропущенного) шага.
|
|
209
223
|
if (this.navigator.total === 0) {
|
|
224
|
+
this.logger.warn('Нет ни одного видимого шага, тур не показан');
|
|
210
225
|
this.status = 'idle';
|
|
211
226
|
this.releaseStep();
|
|
212
227
|
return;
|
|
213
228
|
}
|
|
214
229
|
const step = this.navigator.current;
|
|
215
230
|
if (step.waitForResume) {
|
|
231
|
+
this.logger.debug('Ожидание resume', { step: step.name });
|
|
216
232
|
this.status = 'waiting-resume';
|
|
217
233
|
this.releaseStep();
|
|
218
234
|
return;
|
|
219
235
|
}
|
|
236
|
+
this.logger.debug('Показан шаг', {
|
|
237
|
+
step: step.name,
|
|
238
|
+
position: this.navigator.position,
|
|
239
|
+
total: this.navigator.total,
|
|
240
|
+
});
|
|
220
241
|
this.status = 'running';
|
|
221
242
|
this.publishStep();
|
|
222
243
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from '../../../../services/Logger';
|
|
1
2
|
import { type SaveStrategy, type TourProgress } from '../../types';
|
|
2
3
|
/**
|
|
3
4
|
* Обёртка над переданной SaveStrategy, изолирующая тур от исключений хранилища
|
|
@@ -9,8 +10,9 @@ import { type SaveStrategy, type TourProgress } from '../../types';
|
|
|
9
10
|
*/
|
|
10
11
|
export declare class ProgressStorage {
|
|
11
12
|
private readonly strategy;
|
|
13
|
+
private readonly logger;
|
|
12
14
|
private saving;
|
|
13
|
-
constructor(strategy: SaveStrategy);
|
|
15
|
+
constructor(strategy: SaveStrategy, logger: Logger);
|
|
14
16
|
load(): Promise<TourProgress | null>;
|
|
15
17
|
save(state: TourProgress): Promise<void>;
|
|
16
18
|
/**
|
|
@@ -18,8 +18,9 @@ const CLEARED_STATE = {
|
|
|
18
18
|
* `load()` ждёт текущее сохранение, поэтому читает уже применённое состояние.
|
|
19
19
|
*/
|
|
20
20
|
class ProgressStorage {
|
|
21
|
-
constructor(strategy) {
|
|
21
|
+
constructor(strategy, logger) {
|
|
22
22
|
this.strategy = strategy;
|
|
23
|
+
this.logger = logger;
|
|
23
24
|
this.saving = Promise.resolve();
|
|
24
25
|
}
|
|
25
26
|
async load() {
|
|
@@ -28,7 +29,9 @@ class ProgressStorage {
|
|
|
28
29
|
try {
|
|
29
30
|
return await this.strategy.load();
|
|
30
31
|
}
|
|
31
|
-
catch {
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Ошибка хранилища не должна ронять тур, но остаётся видимой в логах.
|
|
34
|
+
this.logger.warn('Ошибка чтения прогресса из хранилища', error);
|
|
32
35
|
return null;
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -52,8 +55,9 @@ class ProgressStorage {
|
|
|
52
55
|
try {
|
|
53
56
|
await write();
|
|
54
57
|
}
|
|
55
|
-
catch {
|
|
56
|
-
// Ошибка хранилища не должна ронять
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Ошибка хранилища не должна ронять тур, но остаётся видимой в логах.
|
|
60
|
+
this.logger.warn('Ошибка записи прогресса в хранилище', error);
|
|
57
61
|
}
|
|
58
62
|
});
|
|
59
63
|
return this.saving;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export type LogThreshold = LogLevel | 'silent';
|
|
3
|
+
export type LogContext = Readonly<Record<string, unknown>>;
|
|
4
|
+
export type LoggerOptions = {
|
|
5
|
+
context?: LogContext;
|
|
6
|
+
};
|
|
7
|
+
export type CreateLoggerOptions = LoggerOptions & {
|
|
8
|
+
name: string;
|
|
9
|
+
level?: LogThreshold;
|
|
10
|
+
};
|
|
11
|
+
export type ConfigureOptions = {
|
|
12
|
+
level?: LogThreshold;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Логгер.
|
|
16
|
+
* Формат логов: `{пакет}: {иконка} [{scope}]: {сообщение}`.
|
|
17
|
+
* */
|
|
18
|
+
export declare class Logger {
|
|
19
|
+
private readonly name;
|
|
20
|
+
private readonly scope;
|
|
21
|
+
private readonly context;
|
|
22
|
+
private readonly parent;
|
|
23
|
+
private level?;
|
|
24
|
+
private readonly console;
|
|
25
|
+
private constructor();
|
|
26
|
+
static create: ({ name, context, level, }: CreateLoggerOptions) => Logger;
|
|
27
|
+
/**
|
|
28
|
+
* Меняет уровень пакета.
|
|
29
|
+
* Без `level` — сброс к env-дефолту.
|
|
30
|
+
* Возвращает функцию отмены — вернёт прежний уровень.
|
|
31
|
+
* */
|
|
32
|
+
configure: (options?: ConfigureOptions) => () => void;
|
|
33
|
+
/** Дочерний логгер: добавляет `scope`, наследует уровень корня. */
|
|
34
|
+
child: (scope: string, options?: LoggerOptions) => Logger;
|
|
35
|
+
/** Дойдёт ли лог такого уровня до консоли. */
|
|
36
|
+
isLevelEnabled: (level: LogLevel) => boolean;
|
|
37
|
+
success: (message: string, ...data: unknown[]) => void;
|
|
38
|
+
info: (message: string, ...data: unknown[]) => void;
|
|
39
|
+
warn: (message: string, ...data: unknown[]) => void;
|
|
40
|
+
debug: (message: string, ...data: unknown[]) => void;
|
|
41
|
+
error: (message: string, ...data: unknown[]) => void;
|
|
42
|
+
/** Свой уровень → уровень родителя по цепочке → env-дефолт. */
|
|
43
|
+
private effectiveLevel;
|
|
44
|
+
private print;
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// biome-ignore-all lint/suspicious/noConsole: Logger — единственная точка вывода в консоль
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.Logger = void 0;
|
|
5
|
+
/** Приоритет уровней. Лог виден, если его приоритет ≤ порога. */
|
|
6
|
+
const LOG_LEVEL_PRIORITY = {
|
|
7
|
+
silent: 0,
|
|
8
|
+
error: 1,
|
|
9
|
+
warn: 2,
|
|
10
|
+
info: 3,
|
|
11
|
+
debug: 4,
|
|
12
|
+
};
|
|
13
|
+
/** Порог по умолчанию для каждого окружения. */
|
|
14
|
+
const DEFAULT_LOG_LEVEL_BY_ENV = {
|
|
15
|
+
production: 'error',
|
|
16
|
+
test: 'silent',
|
|
17
|
+
development: 'warn',
|
|
18
|
+
};
|
|
19
|
+
/** Порог для текущего окружения. */
|
|
20
|
+
const getEnvLevel = () => DEFAULT_LOG_LEVEL_BY_ENV[process.env.NODE_ENV ?? 'development'] ?? 'warn';
|
|
21
|
+
/**
|
|
22
|
+
* Логгер.
|
|
23
|
+
* Формат логов: `{пакет}: {иконка} [{scope}]: {сообщение}`.
|
|
24
|
+
* */
|
|
25
|
+
class Logger {
|
|
26
|
+
constructor(name, scope, context, parent, level, console = globalThis.console) {
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.scope = scope;
|
|
29
|
+
this.context = context;
|
|
30
|
+
this.parent = parent;
|
|
31
|
+
this.level = level;
|
|
32
|
+
this.console = console;
|
|
33
|
+
/**
|
|
34
|
+
* Меняет уровень пакета.
|
|
35
|
+
* Без `level` — сброс к env-дефолту.
|
|
36
|
+
* Возвращает функцию отмены — вернёт прежний уровень.
|
|
37
|
+
* */
|
|
38
|
+
this.configure = (options = {}) => {
|
|
39
|
+
const previousLevel = this.level;
|
|
40
|
+
this.level = options.level;
|
|
41
|
+
return () => {
|
|
42
|
+
this.level = previousLevel;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** Дочерний логгер: добавляет `scope`, наследует уровень корня. */
|
|
46
|
+
this.child = (scope, options) => {
|
|
47
|
+
return new Logger(this.name,
|
|
48
|
+
// склеиваем scope: `Onboarding` → `Onboarding Tour`.
|
|
49
|
+
this.scope ? `${this.scope} ${scope}` : scope, { ...this.context, ...options?.context }, this, undefined, this.console);
|
|
50
|
+
};
|
|
51
|
+
/** Дойдёт ли лог такого уровня до консоли. */
|
|
52
|
+
this.isLevelEnabled = (level) => LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.effectiveLevel()];
|
|
53
|
+
this.success = (message, ...data) => this.print('info', '✅', this.console.log, message, data);
|
|
54
|
+
this.info = (message, ...data) => this.print('info', 'ℹ️', this.console.log, message, data);
|
|
55
|
+
this.warn = (message, ...data) => this.print('warn', '⚠️', this.console.warn, message, data);
|
|
56
|
+
this.debug = (message, ...data) => this.print('debug', '🐞', this.console.log, message, data);
|
|
57
|
+
this.error = (message, ...data) => this.print('error', '❌', this.console.error, message, data);
|
|
58
|
+
/** Свой уровень → уровень родителя по цепочке → env-дефолт. */
|
|
59
|
+
this.effectiveLevel = () => this.level ?? this.parent?.effectiveLevel() ?? getEnvLevel();
|
|
60
|
+
this.print = (level, icon, sink, message, data) => {
|
|
61
|
+
if (!this.isLevelEnabled(level)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const prefix = this.name ? `${this.name}: ` : '';
|
|
65
|
+
const scope = this.scope ? ` [${this.scope}]:` : '';
|
|
66
|
+
const withContext = Object.keys(this.context).length === 0 ? data : [...data, this.context];
|
|
67
|
+
sink(`${prefix}${icon}${scope} ${message}`, ...withContext);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.Logger = Logger;
|
|
72
|
+
Logger.create = ({ name, context = {}, level, }) => new Logger(name, undefined, { ...context }, null, level);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Logger, type ConfigureOptions, type CreateLoggerOptions, type LogContext, type LoggerOptions, type LogLevel, type LogThreshold } from './Logger';
|
|
2
|
+
export { Logger, type ConfigureOptions, type CreateLoggerOptions, type LogContext, type LoggerOptions, type LogLevel, type LogThreshold, };
|
|
3
|
+
/**
|
|
4
|
+
* Фабрика Logger для `@astral/ui`: сервисы и компоненты получают инстанс через DI.
|
|
5
|
+
*/
|
|
6
|
+
export declare const createLogger: (scope: string, options?: LoggerOptions) => Logger;
|
|
7
|
+
/**
|
|
8
|
+
* (Пере)-Настройка логгера пакета `@astral/ui` — вызывает `ConfigProvider`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const configureLogger: (options?: ConfigureOptions) => () => void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureLogger = exports.createLogger = exports.Logger = void 0;
|
|
4
|
+
const Logger_1 = require("./Logger");
|
|
5
|
+
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return Logger_1.Logger; } });
|
|
6
|
+
// Корневой логгер пакета @astral/ui.
|
|
7
|
+
const uiLogger = Logger_1.Logger.create({ name: '@astral/ui' });
|
|
8
|
+
/**
|
|
9
|
+
* Фабрика Logger для `@astral/ui`: сервисы и компоненты получают инстанс через DI.
|
|
10
|
+
*/
|
|
11
|
+
const createLogger = (scope, options) => uiLogger.child(scope, options);
|
|
12
|
+
exports.createLogger = createLogger;
|
|
13
|
+
/**
|
|
14
|
+
* (Пере)-Настройка логгера пакета `@astral/ui` — вызывает `ConfigProvider`.
|
|
15
|
+
*/
|
|
16
|
+
const configureLogger = (options) => uiLogger.configure(options);
|
|
17
|
+
exports.configureLogger = configureLogger;
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export type LogThreshold = LogLevel | 'silent';
|
|
3
|
+
export type LogContext = Readonly<Record<string, unknown>>;
|
|
4
|
+
export type LoggerOptions = {
|
|
5
|
+
context?: LogContext;
|
|
6
|
+
};
|
|
7
|
+
export type CreateLoggerOptions = LoggerOptions & {
|
|
8
|
+
name: string;
|
|
9
|
+
level?: LogThreshold;
|
|
10
|
+
};
|
|
11
|
+
export type ConfigureOptions = {
|
|
12
|
+
level?: LogThreshold;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Логгер.
|
|
16
|
+
* Формат логов: `{пакет}: {иконка} [{scope}]: {сообщение}`.
|
|
17
|
+
* */
|
|
18
|
+
export declare class Logger {
|
|
19
|
+
private readonly name;
|
|
20
|
+
private readonly scope;
|
|
21
|
+
private readonly context;
|
|
22
|
+
private readonly parent;
|
|
23
|
+
private level?;
|
|
24
|
+
private readonly console;
|
|
25
|
+
private constructor();
|
|
26
|
+
static create: ({ name, context, level, }: CreateLoggerOptions) => Logger;
|
|
27
|
+
/**
|
|
28
|
+
* Меняет уровень пакета.
|
|
29
|
+
* Без `level` — сброс к env-дефолту.
|
|
30
|
+
* Возвращает функцию отмены — вернёт прежний уровень.
|
|
31
|
+
* */
|
|
32
|
+
configure: (options?: ConfigureOptions) => () => void;
|
|
33
|
+
/** Дочерний логгер: добавляет `scope`, наследует уровень корня. */
|
|
34
|
+
child: (scope: string, options?: LoggerOptions) => Logger;
|
|
35
|
+
/** Дойдёт ли лог такого уровня до консоли. */
|
|
36
|
+
isLevelEnabled: (level: LogLevel) => boolean;
|
|
37
|
+
success: (message: string, ...data: unknown[]) => void;
|
|
38
|
+
info: (message: string, ...data: unknown[]) => void;
|
|
39
|
+
warn: (message: string, ...data: unknown[]) => void;
|
|
40
|
+
debug: (message: string, ...data: unknown[]) => void;
|
|
41
|
+
error: (message: string, ...data: unknown[]) => void;
|
|
42
|
+
/** Свой уровень → уровень родителя по цепочке → env-дефолт. */
|
|
43
|
+
private effectiveLevel;
|
|
44
|
+
private print;
|
|
45
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noConsole: Logger — единственная точка вывода в консоль
|
|
2
|
+
/** Приоритет уровней. Лог виден, если его приоритет ≤ порога. */
|
|
3
|
+
const LOG_LEVEL_PRIORITY = {
|
|
4
|
+
silent: 0,
|
|
5
|
+
error: 1,
|
|
6
|
+
warn: 2,
|
|
7
|
+
info: 3,
|
|
8
|
+
debug: 4,
|
|
9
|
+
};
|
|
10
|
+
/** Порог по умолчанию для каждого окружения. */
|
|
11
|
+
const DEFAULT_LOG_LEVEL_BY_ENV = {
|
|
12
|
+
production: 'error',
|
|
13
|
+
test: 'silent',
|
|
14
|
+
development: 'warn',
|
|
15
|
+
};
|
|
16
|
+
/** Порог для текущего окружения. */
|
|
17
|
+
const getEnvLevel = () => DEFAULT_LOG_LEVEL_BY_ENV[process.env.NODE_ENV ?? 'development'] ?? 'warn';
|
|
18
|
+
/**
|
|
19
|
+
* Логгер.
|
|
20
|
+
* Формат логов: `{пакет}: {иконка} [{scope}]: {сообщение}`.
|
|
21
|
+
* */
|
|
22
|
+
export class Logger {
|
|
23
|
+
constructor(name, scope, context, parent, level, console = globalThis.console) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.scope = scope;
|
|
26
|
+
this.context = context;
|
|
27
|
+
this.parent = parent;
|
|
28
|
+
this.level = level;
|
|
29
|
+
this.console = console;
|
|
30
|
+
/**
|
|
31
|
+
* Меняет уровень пакета.
|
|
32
|
+
* Без `level` — сброс к env-дефолту.
|
|
33
|
+
* Возвращает функцию отмены — вернёт прежний уровень.
|
|
34
|
+
* */
|
|
35
|
+
this.configure = (options = {}) => {
|
|
36
|
+
const previousLevel = this.level;
|
|
37
|
+
this.level = options.level;
|
|
38
|
+
return () => {
|
|
39
|
+
this.level = previousLevel;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/** Дочерний логгер: добавляет `scope`, наследует уровень корня. */
|
|
43
|
+
this.child = (scope, options) => {
|
|
44
|
+
return new Logger(this.name,
|
|
45
|
+
// склеиваем scope: `Onboarding` → `Onboarding Tour`.
|
|
46
|
+
this.scope ? `${this.scope} ${scope}` : scope, { ...this.context, ...options?.context }, this, undefined, this.console);
|
|
47
|
+
};
|
|
48
|
+
/** Дойдёт ли лог такого уровня до консоли. */
|
|
49
|
+
this.isLevelEnabled = (level) => LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.effectiveLevel()];
|
|
50
|
+
this.success = (message, ...data) => this.print('info', '✅', this.console.log, message, data);
|
|
51
|
+
this.info = (message, ...data) => this.print('info', 'ℹ️', this.console.log, message, data);
|
|
52
|
+
this.warn = (message, ...data) => this.print('warn', '⚠️', this.console.warn, message, data);
|
|
53
|
+
this.debug = (message, ...data) => this.print('debug', '🐞', this.console.log, message, data);
|
|
54
|
+
this.error = (message, ...data) => this.print('error', '❌', this.console.error, message, data);
|
|
55
|
+
/** Свой уровень → уровень родителя по цепочке → env-дефолт. */
|
|
56
|
+
this.effectiveLevel = () => this.level ?? this.parent?.effectiveLevel() ?? getEnvLevel();
|
|
57
|
+
this.print = (level, icon, sink, message, data) => {
|
|
58
|
+
if (!this.isLevelEnabled(level)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const prefix = this.name ? `${this.name}: ` : '';
|
|
62
|
+
const scope = this.scope ? ` [${this.scope}]:` : '';
|
|
63
|
+
const withContext = Object.keys(this.context).length === 0 ? data : [...data, this.context];
|
|
64
|
+
sink(`${prefix}${icon}${scope} ${message}`, ...withContext);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Logger.create = ({ name, context = {}, level, }) => new Logger(name, undefined, { ...context }, null, level);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Logger, type ConfigureOptions, type CreateLoggerOptions, type LogContext, type LoggerOptions, type LogLevel, type LogThreshold } from './Logger';
|
|
2
|
+
export { Logger, type ConfigureOptions, type CreateLoggerOptions, type LogContext, type LoggerOptions, type LogLevel, type LogThreshold, };
|
|
3
|
+
/**
|
|
4
|
+
* Фабрика Logger для `@astral/ui`: сервисы и компоненты получают инстанс через DI.
|
|
5
|
+
*/
|
|
6
|
+
export declare const createLogger: (scope: string, options?: LoggerOptions) => Logger;
|
|
7
|
+
/**
|
|
8
|
+
* (Пере)-Настройка логгера пакета `@astral/ui` — вызывает `ConfigProvider`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const configureLogger: (options?: ConfigureOptions) => () => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Logger, } from './Logger';
|
|
2
|
+
export { Logger, };
|
|
3
|
+
// Корневой логгер пакета @astral/ui.
|
|
4
|
+
const uiLogger = Logger.create({ name: '@astral/ui' });
|
|
5
|
+
/**
|
|
6
|
+
* Фабрика Logger для `@astral/ui`: сервисы и компоненты получают инстанс через DI.
|
|
7
|
+
*/
|
|
8
|
+
export const createLogger = (scope, options) => uiLogger.child(scope, options);
|
|
9
|
+
/**
|
|
10
|
+
* (Пере)-Настройка логгера пакета `@astral/ui` — вызывает `ConfigProvider`.
|
|
11
|
+
*/
|
|
12
|
+
export const configureLogger = (options) => uiLogger.configure(options);
|