package statistics import ( "context" "database/sql" "fmt" ) type DepsClick struct { Conn *sql.DB } type StatisticClick struct { conn *sql.DB } func NewClickStatistic(ctx context.Context, deps DepsClick) (*StatisticClick, error) { s := &StatisticClick{ conn: deps.Conn, } err := s.checkMW(ctx) if err != nil { fmt.Println("error check material view existing") return nil, err } return s, nil } // todo toanaliz for keydevice,keydevicetype,keybrowser, func (s *StatisticClick) checkMW(ctx context.Context) error { // в мат вью получаем последний ссесионный координат, то есть тот момент на котором сессия в квизе завершилась // не зависит какой это тип вопроса, страт, обычный, резулт, получаем всегда последний у сессии // он определят конечное положение пользователя в опросе query := ` CREATE MATERIALIZED VIEW IF NOT EXISTS mv_last_answers_events ENGINE = MergeTree() ORDER BY (ctxsession, event_time) AS SELECT event_time, ctxsession, ctxquizid, ctxquestionid, ctxidint, message, keyos, ctxuserip, ctxuserport, keydomain, keypath, ctxquiz, ctxreferrer FROM (SELECT event_time, ctxsession, ctxquizid, ctxquestionid, ctxidint, message, keyos, ctxuserip, ctxuserport, keydomain, keypath, ctxquiz, ctxreferrer, row_number() OVER (PARTITION BY ctxsession ORDER BY event_time DESC) as row_num FROM statistics WHERE message IN ('InfoQuizOpen', 'InfoAnswer', 'InfoResult') AND event_level = 'info') AS sorted WHERE row_num = 1; ` _, err := s.conn.ExecContext(ctx, query) if err != nil { return err } return nil } type Statistic struct { Count int64 QuestionID int64 } type PipeLineStatsResp [][]Statistic type FunnelData struct { Session string QuestionIDs []int64 } func (s *StatisticClick) funnelData(ctx context.Context, quizID int64, from uint64, to uint64) ([]FunnelData, error) { // получили все уникальные сессии из мат вью sessionQuery := ` SELECT DISTINCT ctxsession FROM mv_last_answers_events WHERE ctxquizid = ? AND event_time BETWEEN ? AND ? ` sessionRows, err := s.conn.QueryContext(ctx, sessionQuery, quizID, from, to) if err != nil { return nil, err } defer sessionRows.Close() var funnelData []FunnelData for sessionRows.Next() { var ctxsession string err := sessionRows.Scan(&ctxsession) if err != nil { return nil, err } // получаем вопросы уникальные, последние в рамках сессии questionQuery := ` SELECT DISTINCT ctxquestionid FROM statistics WHERE ctxsession = ? ORDER BY event_time DESC; ` questionRows, err := s.conn.QueryContext(ctx, questionQuery, ctxsession) if err != nil { return nil, err } defer questionRows.Close() var questions []int64 for questionRows.Next() { var ctxquestionid int64 err := questionRows.Scan(&ctxquestionid) if err != nil { return nil, err } questions = append(questions, ctxquestionid) } fmt.Println("questions", questions) if err := questionRows.Err(); err != nil { return nil, err } funnelData = append(funnelData, FunnelData{ Session: ctxsession, QuestionIDs: questions, }) } if err := sessionRows.Err(); err != nil { return nil, err } return funnelData, nil } // todo еще раз понять подумать осознать как подписывать воронку сейчас если массив соддержит 1 элемент это старт ответ func (s *StatisticClick) getOneFunnelQuestions(ctx context.Context, quizID int64, from uint64, to uint64) ([]int64, error) { var result []int64 funnelData, err := s.funnelData(ctx, quizID, from, to) if err != nil { return nil, err } for _, fd := range funnelData { if len(fd.QuestionIDs) == 1 { result = append(result, fd.QuestionIDs...) } } return result, nil } func (s *StatisticClick) GetPipelinesStatistics(ctx context.Context, quizID int64, from uint64, to uint64) (PipeLineStatsResp, error) { // тут нужно подписать все вопросы, которые принадлежат окончанию сессии, то есть массив ответов до окончания сессии // это делается для каждой из сессии которая имеется в мат вью, это отношение к результату которого достиг респондент // для того чтобы получить индентификатор воронки, то есть тот который последний этап прохождения был, то на чем все закончилось // нужно из всех подписанных которые подписываются массивом и нужно выбрать из всех подписанных ответов на вопросы, выбрать те // которые имеет всего один элемент в списке, то есть достижение никакого другого вопроса или результата не проходило через // этот ответа кроме попытки достигнуть этотго оттвета или результата, это и являются конечные точки прохождения вопросов и от них считаются воронки и являются индентификаторами воронки // именно те которые с массивом с 1 элементом // нужно вернуть в каждом списке пару, id вопроса и количество ответов на этот вопрос, count - уникальные сессии которые прошли через этот вопрос var pipelines PipeLineStatsResp idS, err := s.getOneFunnelQuestions(ctx, quizID, from, to) if err != nil { return nil, err } for _, questionID := range idS { query := ` SELECT ctxquestionid, COUNT(DISTINCT ctxsession) AS session_count FROM mv_last_answers_events WHERE ctxquizid = ? AND ctxquestionid = ? AND event_time BETWEEN ? AND ? GROUP BY ctxquestionid ` rows, err := s.conn.QueryContext(ctx, query, quizID, questionID, from, to) if err != nil { return nil, err } defer rows.Close() var currentPipeline []Statistic for rows.Next() { var id int64 var count int64 err := rows.Scan(&id, &count) if err != nil { return nil, err } currentPipeline = append(currentPipeline, Statistic{ QuestionID: id, Count: count, }) } if err := rows.Err(); err != nil { return nil, err } if len(currentPipeline) > 0 { pipelines = append(pipelines, currentPipeline) } } return pipelines, nil }