visit.queries.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const { isAfter, subDays, subHours, set, format } = require("date-fns");
  2. const utils = require("../utils");
  3. const redis = require("../redis");
  4. const knex = require("../knex");
  5. async function add(params) {
  6. const data = {
  7. ...params,
  8. country: params.country.toLowerCase(),
  9. referrer: params.referrer.toLowerCase()
  10. };
  11. const nowUTC = new Date().toISOString();
  12. const truncatedNow = nowUTC.substring(0, 10) + " " + nowUTC.substring(11, 14) + "00:00";
  13. return knex.transaction(async (trx) => {
  14. // Create a subquery first that truncates the
  15. const subquery = trx("visits")
  16. .select("visits.*")
  17. .select({
  18. created_at_hours: utils.knexUtils(trx).truncatedTimestamp("created_at", "hour")
  19. })
  20. .where({ link_id: params.id })
  21. .as("subquery");
  22. const visit = await trx
  23. .select("*")
  24. .from(subquery)
  25. .where("created_at_hours", "=", truncatedNow)
  26. .forUpdate()
  27. .first();
  28. if (visit) {
  29. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  30. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  31. await trx("visits")
  32. .where({ id: visit.id })
  33. .increment(`br_${data.browser}`, 1)
  34. .increment(`os_${data.os}`, 1)
  35. .increment("total", 1)
  36. .update({
  37. updated_at: utils.dateToUTC(new Date()),
  38. countries: JSON.stringify({
  39. ...countries,
  40. [data.country]: (countries[data.country] ?? 0) + 1
  41. }),
  42. referrers: JSON.stringify({
  43. ...referrers,
  44. [data.referrer]: (referrers[data.referrer] ?? 0) + 1
  45. })
  46. });
  47. } else {
  48. // This must also happen in the transaction to avoid concurrency
  49. await trx("visits").insert({
  50. [`br_${data.browser}`]: 1,
  51. countries: { [data.country]: 1 },
  52. referrers: { [data.referrer]: 1 },
  53. [`os_${data.os}`]: 1,
  54. total: 1,
  55. link_id: data.id
  56. });
  57. }
  58. return visit;
  59. });
  60. }
  61. async function find(match, total) {
  62. if (match.link_id) {
  63. const key = redis.key.stats(match.link_id);
  64. const cached = await redis.client.get(key);
  65. if (cached) return JSON.parse(cached);
  66. }
  67. const stats = {
  68. lastDay: {
  69. stats: utils.getInitStats(),
  70. views: new Array(24).fill(0),
  71. total: 0
  72. },
  73. lastWeek: {
  74. stats: utils.getInitStats(),
  75. views: new Array(7).fill(0),
  76. total: 0
  77. },
  78. lastMonth: {
  79. stats: utils.getInitStats(),
  80. views: new Array(30).fill(0),
  81. total: 0
  82. },
  83. lastYear: {
  84. stats: utils.getInitStats(),
  85. views: new Array(12).fill(0),
  86. total: 0
  87. }
  88. };
  89. const visitsStream = knex("visits").where(match).stream();
  90. const now = new Date();
  91. const periods = utils.getStatsPeriods(now);
  92. for await (const visit of visitsStream) {
  93. periods.forEach(([type, fromDate]) => {
  94. const isIncluded = isAfter(utils.parseDatetime(visit.created_at), fromDate);
  95. if (!isIncluded) return;
  96. const diffFunction = utils.getDifferenceFunction(type);
  97. const diff = diffFunction(now, utils.parseDatetime(visit.created_at));
  98. const index = stats[type].views.length - diff - 1;
  99. const view = stats[type].views[index];
  100. const period = stats[type].stats;
  101. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  102. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  103. stats[type].stats = {
  104. browser: {
  105. chrome: period.browser.chrome + visit.br_chrome,
  106. edge: period.browser.edge + visit.br_edge,
  107. firefox: period.browser.firefox + visit.br_firefox,
  108. ie: period.browser.ie + visit.br_ie,
  109. opera: period.browser.opera + visit.br_opera,
  110. other: period.browser.other + visit.br_other,
  111. safari: period.browser.safari + visit.br_safari
  112. },
  113. os: {
  114. android: period.os.android + visit.os_android,
  115. ios: period.os.ios + visit.os_ios,
  116. linux: period.os.linux + visit.os_linux,
  117. macos: period.os.macos + visit.os_macos,
  118. other: period.os.other + visit.os_other,
  119. windows: period.os.windows + visit.os_windows
  120. },
  121. country: {
  122. ...period.country,
  123. ...Object.entries(countries).reduce(
  124. (obj, [country, count]) => ({
  125. ...obj,
  126. [country]: (period.country[country] || 0) + count
  127. }),
  128. {}
  129. )
  130. },
  131. referrer: {
  132. ...period.referrer,
  133. ...Object.entries(referrers).reduce(
  134. (obj, [referrer, count]) => ({
  135. ...obj,
  136. [referrer]: (period.referrer[referrer] || 0) + count
  137. }),
  138. {}
  139. )
  140. }
  141. };
  142. stats[type].views[index] += visit.total;
  143. stats[type].total += visit.total;
  144. });
  145. }
  146. const response = {
  147. lastYear: {
  148. stats: utils.statsObjectToArray(stats.lastYear.stats),
  149. views: stats.lastYear.views,
  150. total: stats.lastYear.total
  151. },
  152. lastDay: {
  153. stats: utils.statsObjectToArray(stats.lastDay.stats),
  154. views: stats.lastDay.views,
  155. total: stats.lastDay.total
  156. },
  157. lastMonth: {
  158. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  159. views: stats.lastMonth.views,
  160. total: stats.lastMonth.total
  161. },
  162. lastWeek: {
  163. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  164. views: stats.lastWeek.views,
  165. total: stats.lastWeek.total
  166. },
  167. updatedAt: new Date()
  168. };
  169. if (match.link_id) {
  170. const cacheTime = utils.getStatsCacheTime(total);
  171. const key = redis.key.stats(match.link_id);
  172. redis.client.set(key, JSON.stringify(response), "EX", cacheTime);
  173. }
  174. return response;
  175. };
  176. module.exports = {
  177. add,
  178. find
  179. };