Логирование и диагностика

Маски ввода редко работают изолированно. Они взаимодействуют с DOM, браузерными событиями, пользовательским вводом, фреймворками, системами валидации и динамическими интерфейсами. Ошибки могут проявляться только в определённых браузерах, при вставке текста из буфера обмена, во время IME-ввода, при мобильной клавиатуре или после повторной инициализации компонента.

Без системы диагностики подобные проблемы превращаются в трудноуловимые дефекты:

  • курсор прыгает в неправильную позицию;
  • маска перестаёт реагировать после рендера;
  • символы исчезают при вставке;
  • unmaskedvalue() возвращает неожиданный результат;
  • события complete и incomplete вызываются некорректно;
  • input зависает на длинных строках;
  • React/Vue перерисовывает поле и ломает состояние маски.

Логирование позволяет:

  • анализировать внутреннее состояние маски;
  • отслеживать жизненный цикл поля;
  • диагностировать гонки событий;
  • измерять производительность;
  • выявлять повторные инициализации;
  • анализировать взаимодействие с браузером.

Базовая диагностика состояния маски

Проверка наличия экземпляра Inputmask

После инициализации библиотека сохраняет экземпляр в DOM-элементе.

const input = document.querySelector("#phone");

Inputmask("+7 (999) 999-99-99").mask(input);

console.log(input.inputmask);

Результат содержит внутренний объект:

Inputmask {
  opts: {...},
  maskset: {...},
  undoValue: "",
  el: input,
  ...
}

Проверка полезна при диагностике:

  • повторной инициализации;
  • удаления маски;
  • разрушения DOM;
  • проблем после рендера фреймворка.

Проверка активности маски

if (input.inputmask) {
  console.log("Маска активна");
}

Либо:

console.log(Inputmask.isValid("79991234567", {
  mask: "+7 (999) 999-99-99"
}));

Анализ значения поля

Сравнение value и unmaskedvalue

Одной из самых распространённых проблем является расхождение между отображаемым значением и фактическими данными.

console.log(input.value);
console.log(input.inputmask.unmaskedvalue());

Пример:

value:
+7 (999) 123-45-67

unmaskedvalue:
9991234567

Подобная диагностика особенно важна при:

  • отправке форм;
  • интеграции с API;
  • сериализации данных;
  • серверной валидации.

Логирование состояния заполнения

console.log(input.inputmask.isComplete());

Пример применения:

input.addEventListener("blur", () => {
  console.log("Complete:", input.inputmask.isComplete());
});

Диагностика событий

Логирование input-событий

Inputmask активно использует:

  • keydown;
  • keypress;
  • beforeinput;
  • input;
  • compositionstart;
  • compositionend;
  • paste;
  • cut;
  • blur;
  • focus.

Для анализа конфликтов необходимо логировать последовательность событий.

[
  "keydown",
  "keypress",
  "beforeinput",
  "input",
  "keyup",
  "paste",
  "focus",
  "blur"
].forEach(eventName => {
  input.addEventListener(eventName, event => {
    console.log(eventName, {
      value: input.value,
      key: event.key,
      inputType: event.inputType
    });
  });
});

Анализ порядка событий

Разные браузеры вызывают события по-разному.

Например:

Chrome:
keydown → beforeinput → input → keyup

Safari:
keydown → input → keyup

Android:
beforeinput → input

Некорректная работа маски часто связана именно с различиями последовательности событий.


Диагностика paste

Вставка текста — один из наиболее проблемных сценариев.

input.addEventListener("paste", event => {
  console.log("PASTE");

  const pasted = event.clipboardData.getData("text");

  console.log({
    pasted,
    before: input.value
  });

  setTimeout(() => {
    console.log({
      after: input.value
    });
  });
});

Использование колбэков Inputmask для диагностики

oncomplete

Inputmask({
  mask: "+7 (999) 999-99-99",

  oncomplete() {
    console.log("Маска заполнена");
  }
}).mask(input);

onincomplete

Inputmask({
  mask: "+7 (999) 999-99-99",

  onincomplete() {
    console.log("Маска не заполнена");
  }
}).mask(input);

oncleared

Inputmask({
  mask: "+7 (999) 999-99-99",

  oncleared() {
    console.log("Поле очищено");
  }
}).mask(input);

onKeyValidation

Полезен для анализа посимвольного ввода.

Inputmask({
  mask: "9999",

  onKeyValidation(key, result) {
    console.log({
      key,
      valid: result
    });
  }
}).mask(input);

Глубокая диагностика внутренних структур

Анализ maskset

Внутреннее состояние маски хранится в maskset.

console.log(input.inputmask.maskset);

Основные поля:

{
  validPositions: {...},
  tests: {...},
  buffer: [...],
  p: 5
}

validPositions

Хранит подтверждённые символы.

console.log(
  input.inputmask.maskset.validPositions
);

Пример:

{
  0: { input: "9" },
  1: { input: "9" },
  2: { input: "9" }
}

Используется для диагностики:

  • пропуска символов;
  • удаления;
  • конфликтов optional-масок;
  • проблем caret-позиции.

Анализ буфера

console.log(
  input.inputmask.maskset.buffer
);

Пример:

[
  "+",
  "7",
  " ",
  "(",
  "_",
  "_",
  "_",
  ")"
]

Буфер помогает понять:

  • какие символы реально присутствуют;
  • где находятся placeholder-значения;
  • почему не срабатывает complete.

Диагностика позиции курсора

Получение caret-позиции

const caret = input.inputmask.caret();

console.log(caret);

Результат:

{
  begin: 5,
  end: 5
}

Логирование перемещения курсора

input.addEventListener("click", () => {
  console.log(
    input.inputmask.caret()
  );
});

input.addEventListener("keyup", () => {
  console.log(
    input.inputmask.caret()
  );
});

Диагностика прыгающего курсора

Типичная проблема:

input.value = "123";

Прямое изменение value разрушает внутреннее состояние маски.

Правильный вариант:

input.inputmask.setValue("123");

Для диагностики:

console.log({
  value: input.value,
  caret: input.inputmask.caret(),
  maskset: input.inputmask.maskset
});

Перехват ошибок

try/catch вокруг инициализации

try {
  Inputmask({
    mask: "(999"
  }).mask(input);
} catch (error) {
  console.error(error);
}

Логирование ошибок конфигурации

function createMask(selector, config) {
  try {
    const element = document.querySelector(selector);

    if (!element) {
      throw new Error("Элемент не найден");
    }

    Inputmask(config).mask(element);

    console.log("Mask initialized");
  } catch (error) {
    console.error({
      selector,
      config,
      error
    });
  }
}

Диагностика динамических масок

Логирование выбора маски

Inputmask({
  mask: [
    "+7 (999) 999-99-99",
    "+1 (999) 999-9999"
  ],

  onBeforeMask(value) {
    console.log("Before mask:", value);

    return value;
  }
}).mask(input);

Анализ dispatch

Inputmask({
  mask: [
    "9999",
    "99999"
  ],

  dispatch(appended, dynamicMasked) {
    console.log({
      appended,
      value: dynamicMasked.value
    });

    return dynamicMasked.compiledMasks[0];
  }
});

Диагностика regex-масок

Regex-маски особенно сложны для отладки.

Inputmask({
  regex: "[A-Za-z]{3}[0-9]{2}"
}).mask(input);

Для анализа:

input.addEventListener("input", () => {
  console.log({
    value: input.value,
    complete: input.inputmask.isComplete()
  });
});

Диагностика производительности

Измерение времени инициализации

console.time("mask-init");

Inputmask({
  mask: "+7 (999) 999-99-99"
}).mask(input);

console.timeEnd("mask-init");

Анализ массовой инициализации

console.time("all-masks");

document.querySelectorAll("input").forEach(input => {
  Inputmask("9999").mask(input);
});

console.timeEnd("all-masks");

Профилирование input-событий

input.addEventListener("input", () => {
  performance.mark("input-start");

  requestAnimationFrame(() => {
    performance.mark("input-end");

    performance.measure(
      "mask-input",
      "input-start",
      "input-end"
    );

    console.log(
      performance.getEntriesByName("mask-input")
    );
  });
});

Диагностика утечек памяти

Проверка повторной инициализации

Опасный сценарий:

setInterval(() => {
  Inputmask("9999").mask(input);
}, 1000);

Диагностика:

console.count("MASK_INIT");

Проверка remove()

input.inputmask.remove();

После удаления:

console.log(input.inputmask);

Должно быть:

undefined

Анализ DOM-утечек

const observer = new MutationObserver(records => {
  console.log(records);
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

Позволяет выявлять:

  • удаление input без remove();
  • накопление detached DOM;
  • повторные mount/unmount.

Диагностика во фреймворках

React

Основная проблема — повторный рендер.

Неправильно:

<Input value={state} />

Правильно диагностировать через:

useEffect(() => {
  console.log("mounted");

  return () => {
    console.log("unmounted");
  };
}, []);

Проверка повторного mount

useEffect(() => {
  console.count("MASK_CREATE");

  Inputmask("9999").mask(ref.current);
}, []);

Vue

onMounted(() => {
  console.log("mounted");
});

onUnmounted(() => {
  console.log("unmounted");
});

Диагностика IME и мобильных клавиатур

composition-события

Для японского, китайского и корейского ввода:

input.addEventListener(
  "compositionstart",
  () => {
    console.log("compositionstart");
  }
);

input.addEventListener(
  "compositionupdate",
  event => {
    console.log(
      "compositionupdate",
      event.data
    );
  }
);

input.addEventListener(
  "compositionend",
  event => {
    console.log(
      "compositionend",
      event.data
    );
  }
);

Диагностика Android-проблем

Android часто нарушает стандартную последовательность событий.

Полезное логирование:

[
  "keydown",
  "beforeinput",
  "input",
  "keyup"
].forEach(eventName => {
  input.addEventListener(eventName, event => {
    console.log({
      event: eventName,
      value: input.value,
      inputType: event.inputType
    });
  });
});

Создание универсального debug-режима

Обёртка для диагностики

function createDebugMask(element, options) {
  console.log("INIT", element);

  const mask = new Inputmask({
    ...options,

    oncomplete() {
      console.log("COMPLETE");
    },

    onincomplete() {
      console.log("INCOMPLETE");
    },

    oncleared() {
      console.log("CLEARED");
    },

    onKeyValidation(key, valid) {
      console.log("KEY", {
        key,
        valid
      });
    }
  });

  mask.mask(element);

  [
    "focus",
    "blur",
    "input",
    "paste"
  ].forEach(eventName => {
    element.addEventListener(eventName, event => {
      console.log(eventName, {
        value: element.value,
        caret: element.inputmask.caret()
      });
    });
  });

  return mask;
}

Интеграция с системами логирования

Логирование в Sentry

try {
  Inputmask(config).mask(input);
} catch (error) {
  Sentry.captureException(error, {
    extra: {
      config,
      value: input.value
    }
  });
}

Логирование в console.group

console.group("Inputmask");

console.log("Value:", input.value);
console.log(
  "Unmasked:",
  input.inputmask.unmaskedvalue()
);

console.log(
  "Complete:",
  input.inputmask.isComplete()
);

console.groupEnd();

Типичные диагностические сценарии

Маска не применяется

Проверка:

console.log(typeof Inputmask);

Должно быть:

function

Также:

console.log(input);

Символы не вводятся

Диагностика:

input.addEventListener("keydown", e => {
  console.log(e.key);
});

Проверка:

console.log(input.disabled);
console.log(input.readOnly);

Маска исчезает после рендера

Причина:

  • DOM заменён;
  • input пересоздан;
  • framework remount.

Диагностика:

console.count("render");

Не работает complete

Проверка:

console.log(
  input.inputmask.maskset.validPositions
);

Часто причина:

  • optional-символы;
  • greedy=false;
  • placeholder;
  • regex-конфликт.

Некорректная отправка формы

Диагностика:

form.addEventListener("submit", event => {
  console.log({
    masked: input.value,
    unmasked:
      input.inputmask.unmaskedvalue()
  });
});

Расширенное трассирование

Proxy для диагностики

const original = Inputmask.prototype.writeBuffer;

Inputmask.prototype.writeBuffer = function(...args) {
  console.log("writeBuffer", args);

  return original.apply(this, args);
};

Перехват setValue

const originalSetValue =
  HTMLInputElement.prototype.value;

Object.defineProperty(
  HTMLInputElement.prototype,
  "value",
  {
    set(value) {
      console.log("SET VALUE", value);

      return originalSetValue.call(this, value);
    }
  }
);

Создание диагностической панели

Вывод состояния в интерфейс

setInterval(() => {
  debug.innerHTML = `
    <pre>
value: ${input.value}
unmasked: ${input.inputmask.unmaskedvalue()}
complete: ${input.inputmask.isComplete()}
caret: ${
  JSON.stringify(
    input.inputmask.caret(),
    null,
    2
  )
}
    </pre>
  `;
}, 100);

Практика пошаговой диагностики

Этап 1. Проверка подключения

console.log(Inputmask);

Этап 2. Проверка DOM

console.log(input);

Этап 3. Проверка инициализации

console.log(input.inputmask);

Этап 4. Проверка событий

input.addEventListener("input", () => {
  console.log(input.value);
});

Этап 5. Проверка состояния маски

console.log(
  input.inputmask.maskset
);

Этап 6. Проверка производительности

console.time("typing");

Этап 7. Проверка удаления

input.inputmask.remove();